Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c4d935d90 | ||
|
|
90043fe84d | ||
|
|
a6a98ff63e | ||
|
|
911652133b | ||
|
|
cee1b5f522 | ||
|
|
62f5a23fcb | ||
|
|
eb595cdc3e | ||
|
|
572cd065ed | ||
|
|
76dc8a0897 | ||
|
|
4723de6269 | ||
|
|
e15fa35bad | ||
|
|
2cc6bc8ce4 | ||
|
|
5d19d31b2c | ||
|
|
c1b95ede07 | ||
|
|
058185c7fd | ||
|
|
6fb125cf0f | ||
|
|
a945e9b005 | ||
|
|
b943638afb | ||
|
|
207dc0e2bb | ||
|
|
359fbd4738 | ||
|
|
f7700acce4 | ||
|
|
87a3e2d022 | ||
|
|
5d17663040 | ||
|
|
cff3c72f94 | ||
|
|
fadf475f06 | ||
|
|
7228499737 | ||
|
|
bca467a634 | ||
|
|
cb72d2ac80 | ||
|
|
3c79607d1f | ||
|
|
36ad1f16e4 | ||
|
|
5d4f334505 | ||
|
|
1fdb5ba748 | ||
|
|
26df6f51ef | ||
|
|
6caf794ae1 | ||
|
|
2692953e31 | ||
|
|
242fd713ab | ||
|
|
7a12c4d5e2 | ||
|
|
f256ef43c0 | ||
|
|
e0cde2d6ff | ||
|
|
e4e77dc0d2 | ||
|
|
8ba6467f21 | ||
|
|
088cb54317 | ||
|
|
c47bcf665d | ||
|
|
bc90c27e27 | ||
|
|
c1be57773a | ||
|
|
315c68912c | ||
|
|
559d203996 | ||
|
|
54522518c3 | ||
|
|
bacb1561d5 | ||
|
|
b8521f926f | ||
|
|
b29672f4b4 | ||
|
|
a32fe6a41f | ||
|
|
0e35135093 | ||
|
|
31ecace083 | ||
|
|
e5b134f8f4 | ||
|
|
139a4863d1 |
No files matched your search
@@ -0,0 +1 @@
|
||||
.gitignore
|
||||
@@ -19,7 +19,7 @@ on:
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
run-docker:
|
||||
if: github.repository_owner == 'modrinth'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
wait:
|
||||
if: github.repository_owner == 'modrinth'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
permissions:
|
||||
contents: read
|
||||
deployments: write
|
||||
|
||||
@@ -17,10 +17,7 @@ on:
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./apps/labrinth
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
@@ -38,8 +35,6 @@ jobs:
|
||||
- name: Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v2
|
||||
env:
|
||||
SQLX_OFFLINE: true
|
||||
with:
|
||||
file: ./apps/labrinth/Dockerfile
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
name: Modrinth App build
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- .github/workflows/theseus-build.yml
|
||||
- 'apps/app/**'
|
||||
- 'apps/app-frontend/**'
|
||||
- 'packages/app-lib/**'
|
||||
- 'packages/app-macros/**'
|
||||
- 'packages/assets/**'
|
||||
- 'packages/ui/**'
|
||||
- 'packages/utils/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sign-windows-binaries:
|
||||
description: Sign Windows binaries
|
||||
type: boolean
|
||||
default: true
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-latest, windows-latest, blacksmith-4vcpu-ubuntu-2204]
|
||||
include:
|
||||
- platform: macos-latest
|
||||
artifact-target-name: universal-apple-darwin
|
||||
- platform: windows-latest
|
||||
artifact-target-name: x86_64-pc-windows-msvc
|
||||
- platform: blacksmith-4vcpu-ubuntu-2204
|
||||
artifact-target-name: x86_64-unknown-linux-gnu
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- name: 📥 Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 🧰 Setup Rust toolchain
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
rustflags: ''
|
||||
target: ${{ startsWith(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
|
||||
|
||||
- name: 🧰 Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: 🧰 Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
cache: pnpm
|
||||
|
||||
- name: 🧰 Install Linux build dependencies
|
||||
if: startsWith(matrix.platform, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: 🧰 Setup Dasel
|
||||
uses: jaxxstorm/action-install-gh-release@v2.1.0
|
||||
with:
|
||||
repo: TomWright/dasel
|
||||
tag: v2.8.1
|
||||
extension-matching: disable
|
||||
rename-to: ${{ startsWith(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
|
||||
chmod: 0755
|
||||
|
||||
- name: ⚙️ Set application version
|
||||
shell: bash
|
||||
run: |
|
||||
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
|
||||
echo "Setting application version to $APP_VERSION"
|
||||
dasel put -f apps/app/Cargo.toml -t string -v "${APP_VERSION#v}" 'package.version'
|
||||
dasel put -f packages/app-lib/Cargo.toml -t string -v "${APP_VERSION#v}" 'package.version'
|
||||
dasel put -f apps/app-frontend/package.json -t string -v "${APP_VERSION#v}" 'version'
|
||||
|
||||
- name: 💨 Setup Turbo cache
|
||||
uses: rharkor/caching-for-turbo@v1.8
|
||||
|
||||
- name: 🧰 Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: ✍️ Set up Windows code signing
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
shell: bash
|
||||
run: |
|
||||
if [ '${{ startsWith(github.ref, 'refs/tags/v') || inputs.sign-windows-binaries }}' = 'true' ]; then
|
||||
choco install jsign --ignore-dependencies # GitHub runners come with a global Java installation already
|
||||
else
|
||||
dasel delete -f apps/app/tauri-release.conf.json 'bundle.windows.signCommand'
|
||||
fi
|
||||
|
||||
- name: 🔨 Build macOS app
|
||||
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
env:
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: 🔨 Build Linux app
|
||||
run: pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json
|
||||
if: startsWith(matrix.platform, 'ubuntu')
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: 🔨 Build Windows app
|
||||
run: |
|
||||
[System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream
|
||||
$env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD"
|
||||
$env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
|
||||
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis,updater'
|
||||
Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
DIGICERT_ONE_SIGNER_API_KEY: ${{ secrets.DIGICERT_ONE_SIGNER_API_KEY }}
|
||||
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64 }}
|
||||
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD }}
|
||||
|
||||
- name: 📤 Upload app bundles
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: App bundle (${{ matrix.artifact-target-name }})
|
||||
path: |
|
||||
target/release/bundle/appimage/Modrinth App_*.AppImage*
|
||||
target/release/bundle/deb/Modrinth App_*.deb*
|
||||
target/release/bundle/rpm/Modrinth App-*.rpm*
|
||||
target/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz*
|
||||
target/universal-apple-darwin/release/bundle/dmg/Modrinth App_*.dmg*
|
||||
target/release/bundle/nsis/Modrinth App_*-setup.exe*
|
||||
target/release/bundle/nsis/Modrinth App_*-setup.nsis.zip*
|
||||
@@ -1,186 +1,118 @@
|
||||
name: 'Modrinth App build'
|
||||
name: Modrinth App release
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
paths:
|
||||
- .github/workflows/theseus-release.yml
|
||||
- 'apps/app/**'
|
||||
- 'apps/app-frontend/**'
|
||||
- 'packages/app-lib/**'
|
||||
- 'packages/app-macros/**'
|
||||
- 'packages/assets/**'
|
||||
- 'packages/ui/**'
|
||||
- 'packages/utils/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sign-windows-binaries:
|
||||
description: Sign Windows binaries
|
||||
type: boolean
|
||||
default: true
|
||||
required: false
|
||||
version-tag:
|
||||
description: Version tag to release to the wide public
|
||||
type: string
|
||||
required: true
|
||||
release-notes:
|
||||
description: Release notes to include in the Tauri version manifest
|
||||
default: A new release of the Modrinth App is available!
|
||||
type: string
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-latest, windows-latest, ubuntu-22.04]
|
||||
release:
|
||||
name: Release Modrinth App
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
LINUX_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-unknown-linux-gnu)
|
||||
WINDOWS_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-pc-windows-msvc)
|
||||
MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME: App bundle (universal-apple-darwin)
|
||||
LAUNCHER_FILES_BUCKET_BASE_URL: https://launcher-files.modrinth.com
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Rust setup (mac)
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
- name: 📥 Download Modrinth App artifacts
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
rustflags: ''
|
||||
target: x86_64-apple-darwin
|
||||
workflow: theseus-build.yml
|
||||
workflow_conclusion: success
|
||||
event: push
|
||||
branch: ${{ inputs.version-tag }}
|
||||
use_unzip: true
|
||||
|
||||
- name: Rust setup
|
||||
if: "!startsWith(matrix.platform, 'macos')"
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
with:
|
||||
rustflags: ''
|
||||
|
||||
- name: Setup rust cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
target/**
|
||||
!target/*/release/bundle/*/*.dmg
|
||||
!target/*/release/bundle/*/*.app.tar.gz
|
||||
!target/*/release/bundle/*/*.app.tar.gz.sig
|
||||
!target/release/bundle/*/*.dmg
|
||||
!target/release/bundle/*/*.app.tar.gz
|
||||
!target/release/bundle/*/*.app.tar.gz.sig
|
||||
|
||||
!target/release/bundle/appimage/*.AppImage
|
||||
!target/release/bundle/appimage/*.AppImage.tar.gz
|
||||
!target/release/bundle/appimage/*.AppImage.tar.gz.sig
|
||||
!target/release/bundle/deb/*.deb
|
||||
!target/release/bundle/rpm/*.rpm
|
||||
|
||||
!target/release/bundle/msi/*.msi
|
||||
!target/release/bundle/msi/*.msi.zip
|
||||
!target/release/bundle/msi/*.msi.zip.sig
|
||||
|
||||
!target/release/bundle/nsis/*.exe
|
||||
!target/release/bundle/nsis/*.nsis.zip
|
||||
!target/release/bundle/nsis/*.nsis.zip.sig
|
||||
key: ${{ runner.os }}-rust-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-rust-target-
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Install pnpm via corepack
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable
|
||||
corepack prepare --activate
|
||||
|
||||
- name: Get pnpm store directory
|
||||
id: pnpm-cache
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Setup pnpm cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: startsWith(matrix.platform, 'ubuntu')
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev pkg-config libayatana-appindicator3-dev librsvg2-dev
|
||||
|
||||
- name: Install code signing client (Windows only)
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
run: choco install jsign --ignore-dependencies # GitHub runners come with a global Java installation already
|
||||
|
||||
- name: Install frontend dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Disable Windows code signing for non-final release builds
|
||||
if: ${{ startsWith(matrix.platform, 'windows') && !startsWith(github.ref, 'refs/tags/v') && !inputs.sign-windows-binaries }}
|
||||
run: |
|
||||
jq 'del(.bundle.windows.signCommand)' apps/app/tauri-release.conf.json > apps/app/tauri-release.conf.json.new
|
||||
Move-Item -Path apps/app/tauri-release.conf.json.new -Destination apps/app/tauri-release.conf.json -Force
|
||||
|
||||
- name: build app (macos)
|
||||
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
|
||||
if: startsWith(matrix.platform, 'macos')
|
||||
- name: 🛠️ Generate version manifest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: build app (Linux)
|
||||
run: pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json
|
||||
if: startsWith(matrix.platform, 'ubuntu')
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: build app (Windows)
|
||||
VERSION_TAG: ${{ inputs.version-tag }}
|
||||
RELEASE_NOTES: ${{ inputs.release-notes }}
|
||||
run: |
|
||||
[System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream
|
||||
$env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD"
|
||||
$env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
|
||||
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis,updater'
|
||||
Remove-Item -Path signer-client-cert.p12
|
||||
if: startsWith(matrix.platform, 'windows')
|
||||
# Reference: https://tauri.app/plugin/updater/#server-support
|
||||
jq -nc \
|
||||
--arg versionTag "${VERSION_TAG#v}" \
|
||||
--arg releaseNotes "$RELEASE_NOTES" \
|
||||
--rawfile macOsAarch64UpdateArtifactSignature "${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz.sig" \
|
||||
--rawfile macOsX64UpdateArtifactSignature "${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz.sig" \
|
||||
--rawfile linuxX64UpdateArtifactSignature "${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/appimage/Modrinth App_${VERSION_TAG#v}_amd64.AppImage.tar.gz.sig" \
|
||||
--rawfile windowsX64UpdateArtifactSignature "${WINDOWS_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/nsis/Modrinth App_${VERSION_TAG#v}_x64-setup.nsis.zip.sig" \
|
||||
'{
|
||||
"version": $versionTag,
|
||||
"notes": $releaseNotes,
|
||||
"pub_date": now | todateiso8601,
|
||||
"platforms": {
|
||||
"darwin-aarch64": {
|
||||
"signature": $macOsAarch64UpdateArtifactSignature,
|
||||
"url": @uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/macos/\("Modrinth App.app.tar.gz")",
|
||||
"install_urls": [@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/macos/\("Modrinth App_" + $versionTag + "_universal.dmg")"]
|
||||
},
|
||||
"darwin-x86_64": {
|
||||
"signature": $macOsX64UpdateArtifactSignature,
|
||||
"url": @uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/macos/\("Modrinth App.app.tar.gz")",
|
||||
"install_urls": [@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/macos/\("Modrinth App_" + $versionTag + "_universal.dmg")"]
|
||||
},
|
||||
"linux-x86_64": {
|
||||
"signature": $linuxX64UpdateArtifactSignature,
|
||||
"url": @uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/linux/\("Modrinth App_" + $versionTag + "_amd64.AppImage.tar.gz")",
|
||||
"install_urls": [
|
||||
@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/linux/\("Modrinth App_" + $versionTag + "_amd64.deb")",
|
||||
@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/linux/\("Modrinth App_" + $versionTag + "_amd64.AppImage")",
|
||||
@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/linux/\("Modrinth App-" + $versionTag + "-1.x86_64.rpm")"
|
||||
]
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"signature": $windowsX64UpdateArtifactSignature,
|
||||
"url": @uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/windows/\("Modrinth App_" + $versionTag + "_x64-setup.nsis.zip")",
|
||||
"install_urls": [@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/windows/\("Modrinth App_" + $versionTag + "_x64-setup.exe")"]
|
||||
}
|
||||
}
|
||||
}' > updates.json
|
||||
|
||||
echo "Generated manifest for version ${VERSION_TAG}:"
|
||||
cat updates.json
|
||||
|
||||
- name: 📤 Upload release artifacts
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
DIGICERT_ONE_SIGNER_API_KEY: ${{ secrets.DIGICERT_ONE_SIGNER_API_KEY }}
|
||||
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64 }}
|
||||
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD }}
|
||||
VERSION_TAG: ${{ inputs.version-tag }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.LAUNCHER_FILES_BUCKET_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.LAUNCHER_FILES_BUCKET_SECRET_ACCESS_KEY }}
|
||||
AWS_BUCKET: ${{ secrets.LAUNCHER_FILES_BUCKET_NAME }}
|
||||
AWS_REGION: ${{ secrets.LAUNCHER_FILES_BUCKET_REGION }}
|
||||
AWS_ENDPOINT_URL: ${{ secrets.LAUNCHER_FILES_BUCKET_ENDPOINT_URL }}
|
||||
AWS_PAGER: ''
|
||||
# Work around incompatible checksum behavior with some S3-like object storage providers,
|
||||
# such as Cloudflare R2. See:
|
||||
# - https://developers.cloudflare.com/r2/examples/aws/aws-cli/
|
||||
# - https://developers.cloudflare.com/r2/examples/aws/aws-sdk-java/
|
||||
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
|
||||
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
|
||||
run: |
|
||||
for macosBundleType in 'macos' 'dmg'; do
|
||||
aws s3 cp --recursive \
|
||||
"${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/${macosBundleType}" \
|
||||
"s3://${AWS_BUCKET}/versions/${VERSION_TAG#v}/macos"
|
||||
done
|
||||
|
||||
- name: upload ${{ matrix.platform }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.platform }}
|
||||
path: |
|
||||
target/*/release/bundle/*/*.dmg
|
||||
target/*/release/bundle/*/*.app.tar.gz
|
||||
target/*/release/bundle/*/*.app.tar.gz.sig
|
||||
target/release/bundle/*/*.dmg
|
||||
target/release/bundle/*/*.app.tar.gz
|
||||
target/release/bundle/*/*.app.tar.gz.sig
|
||||
for linuxBundleType in 'appimage' 'deb' 'rpm'; do
|
||||
aws s3 cp --recursive \
|
||||
"${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/${linuxBundleType}" \
|
||||
"s3://${AWS_BUCKET}/versions/${VERSION_TAG#v}/linux"
|
||||
done
|
||||
|
||||
target/release/bundle/*/*.AppImage
|
||||
target/release/bundle/*/*.AppImage.tar.gz
|
||||
target/release/bundle/*/*.AppImage.tar.gz.sig
|
||||
target/release/bundle/*/*.deb
|
||||
target/release/bundle/*/*.rpm
|
||||
for windowsBundleType in 'nsis'; do
|
||||
aws s3 cp --recursive \
|
||||
"${WINDOWS_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/${windowsBundleType}" \
|
||||
"s3://${AWS_BUCKET}/versions/${VERSION_TAG#v}/windows"
|
||||
done
|
||||
|
||||
target/release/bundle/msi/*.msi
|
||||
target/release/bundle/msi/*.msi.zip
|
||||
target/release/bundle/msi/*.msi.zip.sig
|
||||
|
||||
target/release/bundle/nsis/*.exe
|
||||
target/release/bundle/nsis/*.nsis.zip
|
||||
target/release/bundle/nsis/*.nsis.zip.sig
|
||||
aws s3 cp updates.json "s3://${AWS_BUCKET}"
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Lint and Test
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
|
||||
env:
|
||||
# Ensure pnpm output is colored in GitHub Actions logs
|
||||
@@ -76,3 +76,8 @@ jobs:
|
||||
|
||||
- name: 🔍 Lint and test
|
||||
run: pnpm run ci
|
||||
|
||||
- name: 🔍 Verify intl:extract has been run
|
||||
run: |
|
||||
pnpm intl:extract
|
||||
git diff --exit-code */*/src/locales/en-US/index.json
|
||||
@@ -1706,7 +1706,7 @@ dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics-types",
|
||||
"foreign-types 0.5.0",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -2699,15 +2699,6 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||
dependencies = [
|
||||
"foreign-types-shared 0.1.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@@ -2715,7 +2706,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared 0.3.1",
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2729,12 +2720,6 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
@@ -3678,11 +3663,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.5"
|
||||
version = "0.27.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2"
|
||||
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 1.3.1",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
@@ -3692,7 +3676,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-rustls 0.26.2",
|
||||
"tower-service",
|
||||
"webpki-roots 0.26.11",
|
||||
"webpki-roots 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3708,22 +3692,6 @@ dependencies = [
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper 1.6.0",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.14"
|
||||
@@ -4389,7 +4357,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"hex",
|
||||
"hmac",
|
||||
"hyper-tls",
|
||||
"hyper-rustls 0.27.7",
|
||||
"hyper-util",
|
||||
"iana-time-zone",
|
||||
"image",
|
||||
@@ -4986,23 +4954,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework 2.11.1",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
@@ -5577,50 +5528,12 @@ dependencies = [
|
||||
"pathdiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.72"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"openssl-macros",
|
||||
"openssl-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-macros"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e145e1651e858e820e4860f7b9c5e169bc1d8ce1c86043be79fa7b7634821847"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@@ -6850,7 +6763,7 @@ dependencies = [
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"hyper 1.6.0",
|
||||
"hyper-rustls 0.27.5",
|
||||
"hyper-rustls 0.27.7",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
@@ -7980,7 +7893,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"foreign-types 0.5.0",
|
||||
"foreign-types",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2 0.5.2",
|
||||
@@ -8999,7 +8912,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "theseus"
|
||||
version = "0.10.1"
|
||||
version = "1.0.0-local"
|
||||
dependencies = [
|
||||
"ariadne",
|
||||
"async-compression",
|
||||
@@ -9064,7 +8977,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "theseus_gui"
|
||||
version = "0.10.1"
|
||||
version = "1.0.0-local"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"daedalus",
|
||||
@@ -9292,16 +9205,6 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.24.1"
|
||||
|
||||
@@ -67,7 +67,12 @@ heck = "0.5.0"
|
||||
hex = "0.4.3"
|
||||
hickory-resolver = "0.25.2"
|
||||
hmac = "0.12.1"
|
||||
hyper-tls = "0.6.0"
|
||||
hyper-rustls = { version = "0.27.7", default-features = false, features = [
|
||||
"http1",
|
||||
"native-tokio",
|
||||
"ring",
|
||||
"tls12",
|
||||
] }
|
||||
hyper-util = "0.1.14"
|
||||
iana-time-zone = "0.1.63"
|
||||
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@modrinth/app-frontend",
|
||||
"private": true,
|
||||
"version": "0.10.1",
|
||||
"version": "1.0.0-local",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -485,13 +485,13 @@ function handleAuxClick(e) {
|
||||
<ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" />
|
||||
<div class="flex items-center gap-1 ml-3">
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
@click="router.back()"
|
||||
>
|
||||
<LeftArrowIcon />
|
||||
</button>
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
@click="router.forward()"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"asset":{"version":"2.0","generator":"Blockbench 4.12.4 glTF exporter"},"scenes":[{"nodes":[1],"name":"blockbench_export"}],"scene":0,"nodes":[{"rotation":[0,0,0.19509032201612825,0.9807852804032304],"translation":[0.15625,1,0],"name":"Cape","mesh":0},{"children":[0]}],"bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":288,"byteLength":288,"target":34962,"byteStride":12},{"buffer":0,"byteOffset":576,"byteLength":192,"target":34962,"byteStride":8},{"buffer":0,"byteOffset":768,"byteLength":72,"target":34963}],"buffers":[{"byteLength":840,"uri":"data:application/octet-stream;base64,AAAAPQAAAAAAAKA+AAAAPQAAAAAAAKC+AAAAPQAAgL8AAKA+AAAAPQAAgL8AAKC+AAAAvQAAAAAAAKC+AAAAvQAAAAAAAKA+AAAAvQAAgL8AAKC+AAAAvQAAgL8AAKA+AAAAvQAAAAAAAKC+AAAAPQAAAAAAAKC+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAvQAAgL8AAKA+AAAAPQAAgL8AAKA+AAAAvQAAgL8AAKC+AAAAPQAAgL8AAKC+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAvQAAgL8AAKA+AAAAPQAAgL8AAKA+AAAAPQAAAAAAAKC+AAAAvQAAAAAAAKC+AAAAPQAAgL8AAKC+AAAAvQAAgL8AAKC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPAAAgD0AADA+AACAPQAAgDwAAAg/AAAwPgAACD8AAEA+AACAPQAAsD4AAIA9AABAPgAACD8AALA+AAAIPwAAgDwAAAA9AACAPAAAgD0AADA+AAAAPQAAMD4AAIA9AAAwPgAAAD0AAKg+AAAAPQAAMD4AAAAAAACoPgAAAAAAAEA+AACAPQAAMD4AAIA9AABAPgAACD8AADA+AAAIPwAAAAAAAIA9AACAPAAAgD0AAAAAAAAIPwAAgDwAAAg/AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUA"}],"accessors":[{"bufferView":0,"componentType":5126,"count":24,"max":[0.03125,0,0.3125],"min":[-0.03125,-1,-0.3125],"type":"VEC3"},{"bufferView":1,"componentType":5126,"count":24,"max":[1,1,1],"min":[-1,-1,-1],"type":"VEC3"},{"bufferView":2,"componentType":5126,"count":24,"max":[0.34375,0.53125],"min":[0,0],"type":"VEC2"},{"bufferView":3,"componentType":5123,"count":36,"max":[23],"min":[0],"type":"SCALAR"}],"materials":[{"pbrMetallicRoughness":{"metallicFactor":0,"roughnessFactor":1,"baseColorTexture":{"index":0}},"alphaMode":"MASK","alphaCutoff":0.05,"doubleSided":true}],"textures":[{"sampler":0,"source":0,"name":"cape.png"}],"samplers":[{"magFilter":9728,"minFilter":9728,"wrapS":33071,"wrapT":33071}],"images":[{"mimeType":"image/png","uri":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAAAXNSR0IArs4c6QAABCRJREFUaEPtlktoE1EUhm/sIzFpYsBgHyn2oRZt0C5ESxWFYutKNyKIWBBR6kpBd3Ur2J0FRdCiuKmIIG4qLnzgQqTUXSlVqdoXTU0lljRpYtOHI/8dznBnOm1mphMwJWeTzJ0795zznf/cex2MMXam2S/ht38siJ8V1lgd5mOBARerc3rZnmK37rwvCyk2nE6wezMRh+4EncH9B1ql+fkU64rPsaBz5bqh4T7Daxn1Kc5zVNeEePJIci0Aq71bzenY6JChwAnA0OBHQ/OtJLnWNwqAy61R1tO3k891ueRKoDKwtqbv7MGbgCnfRgHQoqG9hyX4hc/yiho+/HNqVPFtdj2jwTpQgd/RKT7/0ZUku/o4qAJw50KYXbzr4e+3BioYzc3kwGzAUCLWFw2+UBiCb3bNTDHiPQeAP3CGNmg/6VcSBpDu3hhvDQpOCwABwrQKsRIsqYAChy/EQAXwlPiZ3a3igNPkXIyTjr8o5L7PPdzGf59c+sV/faeWeIIIAHPJ8M3kc7l1K09LKghWAIgqoPZ7djPFTlxb4D6yAgBOQfntrUVWVeRk44tplXJorOVGkVIJTBCTpw9ECFYBIEkyMfmsAXh3u1qi5MlxbbGX/x1ZSCjBAAwgfPr6h49R5UVavk0FXC2wju5p07s6iqEF0PtqSlEf+bKzDRwdgaDU7AkoySJ5Oo/D6ZRq/H0yyuJ/lxkSheG/FgCNm7kL0BoEAG32sqtY1YIHd2/mGzTMVgD3y2slqjgW9xY6ma9AThAARIMiBtOpjADwTWc0bEkB+FZsSTxTW0KBsGPXx0yvrUpEeHAAQIM7wBJLcu8DwHaP/H8i6VSND6SiSjBlRW4WWUypVIBbIgzjVgB0tpdKqLReS0KVPTMTfH0ra2cEgAmAAEd+l1z52LybqwBQYAQAyZPh6gtDW4jjuC4fHx8wXSm0JDbeI95SRYHiFZlUaWVtPQiKAugl5E8ASAUYiy8vc0C474uGasPE5PHc4g0wK/f4obom6UNimol7kTZwQLANwOuqBokqDEeQf4lfvvnNxZJcBTBAGZplWQcQ3tcgwY9oWlXinRW4ugoAgNAWWe6ocn1QvgyRTUb4RZFVljlY/3hSBYCqb6cCZo8ekuATVRZPI/gQW8FWAI1VHganVgDQUajdA6y2gAgAcZFBjTBSpG0AcAqc3VVmCMDTbxGWZvIRCaMNkJ7pFMCzVQD4liCQ8kRFUlvaBkCvL+wYw2ZmNUgCgLajFhRPJlv3ADuS1VtjPQCk823S574ffN/x1dQqy8dHR5SN2Spcbaymz2mjwNYDAD4IQn3TDpVLQIAqNjwAANRrAdoI/3sAOF7Xe1khCNQGVH1bL0JGJb1R52VtD8gVYHkAuVKpbMWZV0C2yObKunkF5EqlshVnXgHZIpsr6/4DlbxcPydnT74AAAAASUVORK5CYII="}],"meshes":[{"primitives":[{"mode":4,"attributes":{"POSITION":0,"NORMAL":1,"TEXCOORD_0":2},"indices":3,"material":0}]}]}
|
||||
@@ -1 +0,0 @@
|
||||
{"asset":{"generator":"FBX2glTF v0.13.1","version":"2.0"},"scene":0,"buffers":[{"byteLength":71248,"uri":"data:application/octet-stream;base64,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 truncated
|
||||
@@ -1 +0,0 @@
|
||||
{"asset":{"generator":"FBX2glTF v0.13.1","version":"2.0"},"scene":0,"buffers":[{"byteLength":71248,"uri":"data:application/octet-stream;base64,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 truncated
|
||||
@@ -136,7 +136,7 @@ const filteredResults = computed(() => {
|
||||
|
||||
if (sortBy.value === 'Game version') {
|
||||
instances.sort((a, b) => {
|
||||
return a.game_version.localeCompare(b.game_version)
|
||||
return a.game_version.localeCompare(b.game_version, undefined, { numeric: true })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -213,6 +213,17 @@ const filteredResults = computed(() => {
|
||||
instanceMap.set(entry[0], entry[1])
|
||||
})
|
||||
}
|
||||
// default sorting would do 1.20.4 < 1.8.9 because 2 < 8
|
||||
// localeCompare with numeric=true puts 1.8.9 < 1.20.4 because 8 < 20
|
||||
if (group.value === 'Game version') {
|
||||
const sortedEntries = [...instanceMap.entries()].sort((a, b) => {
|
||||
return a[0].localeCompare(b[0], undefined, { numeric: true })
|
||||
})
|
||||
instanceMap.clear()
|
||||
sortedEntries.forEach((entry) => {
|
||||
instanceMap.set(entry[0], entry[1])
|
||||
})
|
||||
}
|
||||
|
||||
return instanceMap
|
||||
})
|
||||
|
||||
@@ -108,7 +108,6 @@ async function testJava() {
|
||||
testingJava.value = true
|
||||
testingJavaSuccess.value = await test_jre(
|
||||
props.modelValue ? props.modelValue.path : '',
|
||||
1,
|
||||
props.version,
|
||||
)
|
||||
testingJava.value = false
|
||||
|
||||
@@ -6,9 +6,9 @@ import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
import { get } from '@/helpers/settings.ts'
|
||||
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
|
||||
import useMemorySlider from '@/composables/useMemorySlider'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
@@ -34,7 +34,7 @@ const envVars = ref(
|
||||
|
||||
const overrideMemorySettings = ref(!!props.instance.memory)
|
||||
const memory = ref(props.instance.memory ?? globalSettings.memory)
|
||||
const maxMemory = Math.floor((await get_max_memory().catch(handleError)) / 1024)
|
||||
const { maxMemory, snapPoints } = await useMemorySlider()
|
||||
|
||||
const editProfileObject = computed(() => {
|
||||
const editProfile: {
|
||||
@@ -156,6 +156,8 @@ const messages = defineMessages({
|
||||
:min="512"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
:snap-points="snapPoints"
|
||||
:snap-range="512"
|
||||
unit="MB"
|
||||
/>
|
||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||
|
||||
@@ -59,7 +59,7 @@ watch(
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="m-0 text-lg font-extrabold text-contrast">Hide nametag</h2>
|
||||
<p class="m-0 mt-1">Disables the nametag above your player on the skins page. page.</p>
|
||||
<p class="m-0 mt-1">Disables the nametag above your player on the skins page.</p>
|
||||
</div>
|
||||
<Toggle id="hide-nametag-skins-page" v-model="settings.hide_nametag_skins_page" />
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { get, set } from '@/helpers/settings.ts'
|
||||
import { ref, watch } from 'vue'
|
||||
import { get_max_memory } from '@/helpers/jre'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { Slider, Toggle } from '@modrinth/ui'
|
||||
import useMemorySlider from '@/composables/useMemorySlider'
|
||||
|
||||
const fetchSettings = await get()
|
||||
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
|
||||
@@ -11,7 +10,7 @@ fetchSettings.envVars = fetchSettings.custom_env_vars.map((x) => x.join('=')).jo
|
||||
|
||||
const settings = ref(fetchSettings)
|
||||
|
||||
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
||||
const { maxMemory, snapPoints } = await useMemorySlider()
|
||||
|
||||
watch(
|
||||
settings,
|
||||
@@ -107,6 +106,8 @@ watch(
|
||||
:min="512"
|
||||
:max="maxMemory"
|
||||
:step="64"
|
||||
:snap-points="snapPoints"
|
||||
:snap-range="512"
|
||||
unit="MB"
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
<script lang="ts">
|
||||
import capeModelUrl from '@/assets/models/cape.gltf?url'
|
||||
import wideModelUrl from '@/assets/models/classic_player.gltf?url'
|
||||
import slimModelUrl from '@/assets/models/slim_player.gltf?url'
|
||||
</script>
|
||||
<template>
|
||||
<UploadSkinModal ref="uploadModal" />
|
||||
<ModalWrapper ref="modal" @on-hide="resetState">
|
||||
@@ -16,9 +11,6 @@ import slimModelUrl from '@/assets/models/slim_player.gltf?url'
|
||||
<div class="max-h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
|
||||
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
||||
<SkinPreviewRenderer
|
||||
:slim-model-src="slimModelUrl"
|
||||
:wide-model-src="wideModelUrl"
|
||||
:cape-model-src="capeModelUrl"
|
||||
:variant="variant"
|
||||
:texture-src="previewSkin || ''"
|
||||
:cape-src="selectedCapeTexture"
|
||||
@@ -126,6 +118,7 @@ import {
|
||||
type Cape,
|
||||
type SkinModel,
|
||||
get_normalized_skin_texture,
|
||||
determineModelType,
|
||||
} from '@/helpers/skins.ts'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import {
|
||||
@@ -261,7 +254,7 @@ async function showNew(e: MouseEvent, skinTextureUrl: string) {
|
||||
mode.value = 'new'
|
||||
currentSkin.value = null
|
||||
uploadedTextureUrl.value = skinTextureUrl
|
||||
variant.value = 'CLASSIC'
|
||||
variant.value = await determineModelType(skinTextureUrl)
|
||||
selectedCape.value = undefined
|
||||
visibleCapeList.value = []
|
||||
initVisibleCapeList()
|
||||
|
||||
@@ -10,9 +10,6 @@ import {
|
||||
} from '@modrinth/ui'
|
||||
import { CheckIcon, XIcon } from '@modrinth/assets'
|
||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||
import capeModelUrl from '@/assets/models/cape.gltf?url'
|
||||
import wideModelUrl from '@/assets/models/classic_player.gltf?url'
|
||||
import slimModelUrl from '@/assets/models/slim_player.gltf?url'
|
||||
|
||||
const modal = useTemplateRef('modal')
|
||||
|
||||
@@ -88,9 +85,6 @@ defineExpose({
|
||||
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
|
||||
<SkinPreviewRenderer
|
||||
v-if="currentSkinTexture"
|
||||
:slim-model-src="slimModelUrl"
|
||||
:wide-model-src="wideModelUrl"
|
||||
:cape-model-src="capeModelUrl"
|
||||
:cape-src="currentCapeTexture"
|
||||
:texture-src="currentSkinTexture"
|
||||
:variant="currentSkinVariant"
|
||||
|
||||
@@ -128,6 +128,14 @@ const messages = defineMessages({
|
||||
id: 'instance.worlds.game_already_open',
|
||||
defaultMessage: 'Instance is already open',
|
||||
},
|
||||
noContact: {
|
||||
id: 'instance.worlds.no_contact',
|
||||
defaultMessage: "Server couldn't be contacted",
|
||||
},
|
||||
incompatibleServer: {
|
||||
id: 'instance.worlds.incompatible_server',
|
||||
defaultMessage: 'Server is incompatible',
|
||||
},
|
||||
copyAddress: {
|
||||
id: 'instance.worlds.copy_address',
|
||||
defaultMessage: 'Copy address',
|
||||
@@ -302,39 +310,33 @@ const messages = defineMessages({
|
||||
</template>
|
||||
</div>
|
||||
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
||||
<template v-if="world.type === 'singleplayer' || serverStatus">
|
||||
<ButtonStyled
|
||||
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
||||
color="red"
|
||||
>
|
||||
<button @click="emit('stop')">
|
||||
<StopCircleIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<button
|
||||
v-tooltip="
|
||||
serverIncompatible
|
||||
? 'Server is incompatible'
|
||||
<ButtonStyled
|
||||
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
||||
color="red"
|
||||
>
|
||||
<button @click="emit('stop')">
|
||||
<StopCircleIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.stopButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-else>
|
||||
<button
|
||||
v-tooltip="
|
||||
!serverStatus
|
||||
? formatMessage(messages.noContact)
|
||||
: serverIncompatible
|
||||
? formatMessage(messages.incompatibleServer)
|
||||
: !supportsQuickPlay
|
||||
? formatMessage(messages.noQuickPlay)
|
||||
: playingOtherWorld || locked
|
||||
? formatMessage(messages.gameAlreadyOpen)
|
||||
: null
|
||||
"
|
||||
:disabled="!supportsQuickPlay || playingOtherWorld || startingInstance"
|
||||
@click="emit('play')"
|
||||
>
|
||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||
<PlayIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<ButtonStyled v-else>
|
||||
<button class="invisible">
|
||||
<PlayIcon aria-hidden="true" />
|
||||
"
|
||||
:disabled="!supportsQuickPlay || playingOtherWorld || startingInstance"
|
||||
@click="emit('play')"
|
||||
>
|
||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||
<PlayIcon v-else aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.playButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { get_max_memory } from '@/helpers/jre.js'
|
||||
import { handleError } from '@/store/notifications.js'
|
||||
|
||||
export default async function () {
|
||||
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
||||
|
||||
const snapPoints = computed(() => {
|
||||
let points = []
|
||||
let memory = 2048
|
||||
|
||||
while (memory <= maxMemory.value) {
|
||||
points.push(memory)
|
||||
memory *= 2
|
||||
}
|
||||
|
||||
return points
|
||||
})
|
||||
|
||||
return { maxMemory, snapPoints }
|
||||
}
|
||||
@@ -36,8 +36,8 @@ export async function get_jre(path) {
|
||||
|
||||
// Tests JRE version by running 'java -version' on it.
|
||||
// Returns true if the version is valid, and matches given (after extraction)
|
||||
export async function test_jre(path, majorVersion, minorVersion) {
|
||||
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion, minorVersion })
|
||||
export async function test_jre(path, majorVersion) {
|
||||
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion })
|
||||
}
|
||||
|
||||
// Automatically installs specified java version
|
||||
|
||||
@@ -2,27 +2,46 @@ import * as THREE from 'three'
|
||||
import type { Skin, Cape } from '../skins'
|
||||
import { get_normalized_skin_texture, determineModelType } from '../skins'
|
||||
import { reactive } from 'vue'
|
||||
import { setupSkinModel, disposeCaches } from '@modrinth/utils'
|
||||
import {
|
||||
setupSkinModel,
|
||||
disposeCaches,
|
||||
loadTexture,
|
||||
applyCapeTexture,
|
||||
createTransparentTexture,
|
||||
} from '@modrinth/utils'
|
||||
import { skinPreviewStorage } from '../storage/skin-preview-storage'
|
||||
import capeModelUrl from '@/assets/models/cape.gltf?url'
|
||||
import wideModelUrl from '@/assets/models/classic_player.gltf?url'
|
||||
import slimModelUrl from '@/assets/models/slim_player.gltf?url'
|
||||
import { headStorage } from '../storage/head-storage'
|
||||
import { ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
||||
|
||||
export interface RenderResult {
|
||||
forwards: string
|
||||
backwards: string
|
||||
}
|
||||
|
||||
export interface RawRenderResult {
|
||||
forwards: Blob
|
||||
backwards: Blob
|
||||
}
|
||||
|
||||
class BatchSkinRenderer {
|
||||
private renderer: THREE.WebGLRenderer
|
||||
private readonly scene: THREE.Scene
|
||||
private readonly camera: THREE.PerspectiveCamera
|
||||
private renderer: THREE.WebGLRenderer | null = null
|
||||
private scene: THREE.Scene | null = null
|
||||
private camera: THREE.PerspectiveCamera | null = null
|
||||
private currentModel: THREE.Group | null = null
|
||||
private readonly width: number
|
||||
private readonly height: number
|
||||
|
||||
constructor(width: number = 360, height: number = 504) {
|
||||
this.width = width
|
||||
this.height = height
|
||||
}
|
||||
|
||||
private initializeRenderer(): void {
|
||||
if (this.renderer) return
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
canvas.width = this.width
|
||||
canvas.height = this.height
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
canvas: canvas,
|
||||
@@ -35,10 +54,10 @@ class BatchSkinRenderer {
|
||||
this.renderer.toneMapping = THREE.NoToneMapping
|
||||
this.renderer.toneMappingExposure = 10.0
|
||||
this.renderer.setClearColor(0x000000, 0)
|
||||
this.renderer.setSize(width, height)
|
||||
this.renderer.setSize(this.width, this.height)
|
||||
|
||||
this.scene = new THREE.Scene()
|
||||
this.camera = new THREE.PerspectiveCamera(20, width / height, 0.4, 1000)
|
||||
this.camera = new THREE.PerspectiveCamera(20, this.width / this.height, 0.4, 1000)
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||
@@ -52,9 +71,12 @@ class BatchSkinRenderer {
|
||||
textureUrl: string,
|
||||
modelUrl: string,
|
||||
capeUrl?: string,
|
||||
capeModelUrl?: string,
|
||||
): Promise<RenderResult> {
|
||||
await this.setupModel(modelUrl, textureUrl, capeModelUrl, capeUrl)
|
||||
): Promise<RawRenderResult> {
|
||||
this.initializeRenderer()
|
||||
|
||||
this.clearScene()
|
||||
|
||||
await this.setupModel(modelUrl, textureUrl, capeUrl)
|
||||
|
||||
const headPart = this.currentModel!.getObjectByName('Head')
|
||||
let lookAtTarget: [number, number, number]
|
||||
@@ -79,35 +101,35 @@ class BatchSkinRenderer {
|
||||
private async renderView(
|
||||
cameraPosition: [number, number, number],
|
||||
lookAtPosition: [number, number, number],
|
||||
): Promise<string> {
|
||||
): Promise<Blob> {
|
||||
if (!this.camera || !this.renderer || !this.scene) {
|
||||
throw new Error('Renderer not initialized')
|
||||
}
|
||||
|
||||
this.camera.position.set(...cameraPosition)
|
||||
this.camera.lookAt(...lookAtPosition)
|
||||
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
this.renderer.domElement.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
resolve(url)
|
||||
} else {
|
||||
reject(new Error('Failed to create blob from canvas'))
|
||||
}
|
||||
}, 'image/png')
|
||||
})
|
||||
const dataUrl = this.renderer.domElement.toDataURL('image/webp', 0.9)
|
||||
const response = await fetch(dataUrl)
|
||||
return await response.blob()
|
||||
}
|
||||
|
||||
private async setupModel(
|
||||
modelUrl: string,
|
||||
textureUrl: string,
|
||||
capeModelUrl?: string,
|
||||
capeUrl?: string,
|
||||
): Promise<void> {
|
||||
if (this.currentModel) {
|
||||
this.scene.remove(this.currentModel)
|
||||
private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
|
||||
if (!this.scene) {
|
||||
throw new Error('Renderer not initialized')
|
||||
}
|
||||
|
||||
const { model } = await setupSkinModel(modelUrl, textureUrl, capeModelUrl, capeUrl)
|
||||
const { model } = await setupSkinModel(modelUrl, textureUrl)
|
||||
|
||||
if (capeUrl) {
|
||||
const capeTexture = await loadTexture(capeUrl)
|
||||
applyCapeTexture(model, capeTexture)
|
||||
} else {
|
||||
const transparentTexture = createTransparentTexture()
|
||||
applyCapeTexture(model, null, transparentTexture)
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
group.add(model)
|
||||
@@ -118,8 +140,39 @@ class BatchSkinRenderer {
|
||||
this.currentModel = group
|
||||
}
|
||||
|
||||
private clearScene(): void {
|
||||
if (!this.scene) return
|
||||
|
||||
while (this.scene.children.length > 0) {
|
||||
const child = this.scene.children[0]
|
||||
this.scene.remove(child)
|
||||
|
||||
if (child instanceof THREE.Mesh) {
|
||||
if (child.geometry) child.geometry.dispose()
|
||||
if (child.material) {
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material.forEach((material) => material.dispose())
|
||||
} else {
|
||||
child.material.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||
directionalLight.castShadow = true
|
||||
directionalLight.position.set(2, 4, 3)
|
||||
this.scene.add(ambientLight)
|
||||
this.scene.add(directionalLight)
|
||||
|
||||
this.currentModel = null
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.renderer.dispose()
|
||||
if (this.renderer) {
|
||||
this.renderer.dispose()
|
||||
}
|
||||
disposeCaches()
|
||||
}
|
||||
}
|
||||
@@ -127,18 +180,33 @@ class BatchSkinRenderer {
|
||||
function getModelUrlForVariant(variant: string): string {
|
||||
switch (variant) {
|
||||
case 'SLIM':
|
||||
return slimModelUrl
|
||||
return SlimPlayerModel
|
||||
case 'CLASSIC':
|
||||
case 'UNKNOWN':
|
||||
default:
|
||||
return wideModelUrl
|
||||
return ClassicPlayerModel
|
||||
}
|
||||
}
|
||||
|
||||
export const map = reactive(new Map<string, RenderResult>())
|
||||
export const headMap = reactive(new Map<string, string>())
|
||||
export const skinBlobUrlMap = reactive(new Map<string, RenderResult>())
|
||||
export const headBlobUrlMap = reactive(new Map<string, string>())
|
||||
const DEBUG_MODE = false
|
||||
|
||||
let sharedRenderer: BatchSkinRenderer | null = null
|
||||
function getSharedRenderer(): BatchSkinRenderer {
|
||||
if (!sharedRenderer) {
|
||||
sharedRenderer = new BatchSkinRenderer()
|
||||
}
|
||||
return sharedRenderer
|
||||
}
|
||||
|
||||
export function disposeSharedRenderer(): void {
|
||||
if (sharedRenderer) {
|
||||
sharedRenderer.dispose()
|
||||
sharedRenderer = null
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
||||
const validKeys = new Set<string>()
|
||||
const validHeadKeys = new Set<string>()
|
||||
@@ -152,7 +220,7 @@ export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
||||
|
||||
try {
|
||||
await skinPreviewStorage.cleanupInvalidKeys(validKeys)
|
||||
await skinPreviewStorage.cleanupInvalidKeys(validHeadKeys)
|
||||
await headStorage.cleanupInvalidKeys(validHeadKeys)
|
||||
} catch (error) {
|
||||
console.warn('Failed to cleanup unused skin previews:', error)
|
||||
}
|
||||
@@ -231,13 +299,17 @@ export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64)
|
||||
outputCtx.drawImage(hatCanvas, 0, 0, 8, 8, 0, 0, size, size)
|
||||
}
|
||||
|
||||
outputCanvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(blob)
|
||||
} else {
|
||||
reject(new Error('Failed to create blob from canvas'))
|
||||
}
|
||||
}, 'image/png')
|
||||
outputCanvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
resolve(blob)
|
||||
} else {
|
||||
reject(new Error('Failed to create blob from canvas'))
|
||||
}
|
||||
},
|
||||
'image/webp',
|
||||
0.9,
|
||||
)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
@@ -254,34 +326,24 @@ export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64)
|
||||
async function generateHeadRender(skin: Skin): Promise<string> {
|
||||
const headKey = `${skin.texture_key}-head`
|
||||
|
||||
if (headMap.has(headKey)) {
|
||||
if (headBlobUrlMap.has(headKey)) {
|
||||
if (DEBUG_MODE) {
|
||||
const url = headMap.get(headKey)!
|
||||
const url = headBlobUrlMap.get(headKey)!
|
||||
URL.revokeObjectURL(url)
|
||||
headMap.delete(headKey)
|
||||
headBlobUrlMap.delete(headKey)
|
||||
} else {
|
||||
return headMap.get(headKey)!
|
||||
return headBlobUrlMap.get(headKey)!
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const cached = await skinPreviewStorage.retrieve(headKey)
|
||||
if (cached && typeof cached === 'string') {
|
||||
headMap.set(headKey, cached)
|
||||
return cached
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to retrieve cached head render:', error)
|
||||
}
|
||||
|
||||
const skinUrl = await get_normalized_skin_texture(skin)
|
||||
const headBlob = await generatePlayerHeadBlob(skinUrl, 64)
|
||||
const headUrl = URL.createObjectURL(headBlob)
|
||||
|
||||
headMap.set(headKey, headUrl)
|
||||
headBlobUrlMap.set(headKey, headUrl)
|
||||
|
||||
try {
|
||||
await skinPreviewStorage.store(headKey, headUrl)
|
||||
await headStorage.store(headKey, headBlob)
|
||||
} catch (error) {
|
||||
console.warn('Failed to store head render in persistent storage:', error)
|
||||
}
|
||||
@@ -294,30 +356,49 @@ export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
|
||||
}
|
||||
|
||||
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
|
||||
const renderer = new BatchSkinRenderer()
|
||||
|
||||
try {
|
||||
const skinKeys = skins.map(
|
||||
(skin) => `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`,
|
||||
)
|
||||
const headKeys = skins.map((skin) => `${skin.texture_key}-head`)
|
||||
|
||||
const [cachedSkinPreviews, cachedHeadPreviews] = await Promise.all([
|
||||
skinPreviewStorage.batchRetrieve(skinKeys),
|
||||
headStorage.batchRetrieve(headKeys),
|
||||
])
|
||||
|
||||
for (let i = 0; i < skins.length; i++) {
|
||||
const skinKey = skinKeys[i]
|
||||
const headKey = headKeys[i]
|
||||
|
||||
const rawCached = cachedSkinPreviews[skinKey]
|
||||
if (rawCached) {
|
||||
const cached: RenderResult = {
|
||||
forwards: URL.createObjectURL(rawCached.forwards),
|
||||
backwards: URL.createObjectURL(rawCached.backwards),
|
||||
}
|
||||
skinBlobUrlMap.set(skinKey, cached)
|
||||
}
|
||||
|
||||
const cachedHead = cachedHeadPreviews[headKey]
|
||||
if (cachedHead) {
|
||||
headBlobUrlMap.set(headKey, URL.createObjectURL(cachedHead))
|
||||
}
|
||||
}
|
||||
|
||||
for (const skin of skins) {
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
|
||||
if (map.has(key)) {
|
||||
if (skinBlobUrlMap.has(key)) {
|
||||
if (DEBUG_MODE) {
|
||||
const result = map.get(key)!
|
||||
const result = skinBlobUrlMap.get(key)!
|
||||
URL.revokeObjectURL(result.forwards)
|
||||
URL.revokeObjectURL(result.backwards)
|
||||
map.delete(key)
|
||||
skinBlobUrlMap.delete(key)
|
||||
} else continue
|
||||
}
|
||||
|
||||
try {
|
||||
const cached = await skinPreviewStorage.retrieve(key)
|
||||
if (cached) {
|
||||
map.set(key, cached)
|
||||
continue
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to retrieve cached skin preview:', error)
|
||||
}
|
||||
const renderer = getSharedRenderer()
|
||||
|
||||
let variant = skin.variant
|
||||
if (variant === 'UNKNOWN') {
|
||||
@@ -331,25 +412,35 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
|
||||
|
||||
const modelUrl = getModelUrlForVariant(variant)
|
||||
const cape: Cape | undefined = capes.find((_cape) => _cape.id === skin.cape_id)
|
||||
const renderResult = await renderer.renderSkin(
|
||||
const rawRenderResult = await renderer.renderSkin(
|
||||
await get_normalized_skin_texture(skin),
|
||||
modelUrl,
|
||||
cape?.texture,
|
||||
capeModelUrl,
|
||||
)
|
||||
|
||||
map.set(key, renderResult)
|
||||
const renderResult: RenderResult = {
|
||||
forwards: URL.createObjectURL(rawRenderResult.forwards),
|
||||
backwards: URL.createObjectURL(rawRenderResult.backwards),
|
||||
}
|
||||
|
||||
skinBlobUrlMap.set(key, renderResult)
|
||||
|
||||
try {
|
||||
await skinPreviewStorage.store(key, renderResult)
|
||||
await skinPreviewStorage.store(key, rawRenderResult)
|
||||
} catch (error) {
|
||||
console.warn('Failed to store skin preview in persistent storage:', error)
|
||||
}
|
||||
|
||||
await generateHeadRender(skin)
|
||||
const headKey = `${skin.texture_key}-head`
|
||||
if (!headBlobUrlMap.has(headKey)) {
|
||||
await generateHeadRender(skin)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
renderer.dispose()
|
||||
disposeSharedRenderer()
|
||||
await cleanupUnusedPreviews(skins)
|
||||
|
||||
await skinPreviewStorage.debugCalculateStorage()
|
||||
await headStorage.debugCalculateStorage()
|
||||
}
|
||||
}
|
||||
@@ -62,16 +62,14 @@ export async function determineModelType(texture: string): Promise<'SLIM' | 'CLA
|
||||
|
||||
context.drawImage(image, 0, 0)
|
||||
|
||||
const armX = 44
|
||||
const armY = 16
|
||||
const armWidth = 4
|
||||
const armX = 54
|
||||
const armY = 20
|
||||
const armWidth = 2
|
||||
const armHeight = 12
|
||||
|
||||
const imageData = context.getImageData(armX, armY, armWidth, armHeight).data
|
||||
|
||||
for (let y = 0; y < armHeight; y++) {
|
||||
const alphaIndex = (3 + y * armWidth) * 4 + 3
|
||||
if (imageData[alphaIndex] !== 0) {
|
||||
for (let index = 1; index <= imageData.length; index++) {
|
||||
//every fourth value in RGBA is the alpha channel
|
||||
if (index % 4 == 0 && imageData[index - 1] !== 0) {
|
||||
resolve('CLASSIC')
|
||||
return
|
||||
}
|
||||
@@ -97,7 +95,11 @@ export async function fixUnknownSkins(list: Skin[]) {
|
||||
|
||||
export function filterDefaultSkins(list: Skin[]) {
|
||||
return list
|
||||
.filter((s) => s.source === 'default' && (!s.name || s.variant === DEFAULT_MODELS[s.name]))
|
||||
.filter(
|
||||
(s) =>
|
||||
s.source === 'default' &&
|
||||
(!s.name || !(s.name in DEFAULT_MODELS) || s.variant === DEFAULT_MODELS[s.name]),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
const aIndex = a.name ? DEFAULT_MODEL_SORTING.indexOf(a.name) : -1
|
||||
const bIndex = b.name ? DEFAULT_MODEL_SORTING.indexOf(b.name) : -1
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
interface StoredHead {
|
||||
blob: Blob
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export class HeadStorage {
|
||||
private dbName = 'head-storage'
|
||||
private version = 1
|
||||
private db: IDBDatabase | null = null
|
||||
|
||||
async init(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, this.version)
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result
|
||||
resolve()
|
||||
}
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result
|
||||
if (!db.objectStoreNames.contains('heads')) {
|
||||
db.createObjectStore('heads')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async store(key: string, blob: Blob): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||
const store = transaction.objectStore('heads')
|
||||
|
||||
const storedHead: StoredHead = {
|
||||
blob,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.put(storedHead, key)
|
||||
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async retrieve(key: string): Promise<string | null> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||
const store = transaction.objectStore('heads')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.get(key)
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as StoredHead | undefined
|
||||
|
||||
if (!result) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(result.blob)
|
||||
resolve(url)
|
||||
}
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async batchRetrieve(keys: string[]): Promise<Record<string, Blob | null>> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||
const store = transaction.objectStore('heads')
|
||||
const results: Record<string, Blob | null> = {}
|
||||
|
||||
return new Promise((resolve, _reject) => {
|
||||
let completedRequests = 0
|
||||
|
||||
if (keys.length === 0) {
|
||||
resolve(results)
|
||||
return
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const request = store.get(key)
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as StoredHead | undefined
|
||||
|
||||
if (result) {
|
||||
results[key] = result.blob
|
||||
} else {
|
||||
results[key] = null
|
||||
}
|
||||
|
||||
completedRequests++
|
||||
if (completedRequests === keys.length) {
|
||||
resolve(results)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
results[key] = null
|
||||
completedRequests++
|
||||
if (completedRequests === keys.length) {
|
||||
resolve(results)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||
const store = transaction.objectStore('heads')
|
||||
let deletedCount = 0
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.openCursor()
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||
|
||||
if (cursor) {
|
||||
const key = cursor.primaryKey as string
|
||||
|
||||
if (!validKeys.has(key)) {
|
||||
const deleteRequest = cursor.delete()
|
||||
deleteRequest.onsuccess = () => {
|
||||
deletedCount++
|
||||
}
|
||||
deleteRequest.onerror = () => {
|
||||
console.warn('Failed to delete invalid head entry:', key)
|
||||
}
|
||||
}
|
||||
|
||||
cursor.continue()
|
||||
} else {
|
||||
resolve(deletedCount)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async debugCalculateStorage(): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||
const store = transaction.objectStore('heads')
|
||||
|
||||
let totalSize = 0
|
||||
let count = 0
|
||||
const entries: Array<{ key: string; size: number }> = []
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.openCursor()
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||
|
||||
if (cursor) {
|
||||
const key = cursor.primaryKey as string
|
||||
const value = cursor.value as StoredHead
|
||||
|
||||
const entrySize = value.blob.size
|
||||
totalSize += entrySize
|
||||
count++
|
||||
|
||||
entries.push({
|
||||
key,
|
||||
size: entrySize,
|
||||
})
|
||||
|
||||
cursor.continue()
|
||||
} else {
|
||||
console.group('🗄️ Head Storage Debug Info')
|
||||
console.log(`Total entries: ${count}`)
|
||||
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
|
||||
console.log(
|
||||
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
|
||||
)
|
||||
|
||||
if (entries.length > 0) {
|
||||
const sortedEntries = entries.sort((a, b) => b.size - a.size)
|
||||
console.log(
|
||||
'Largest entry:',
|
||||
sortedEntries[0].key,
|
||||
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
|
||||
)
|
||||
console.log(
|
||||
'Smallest entry:',
|
||||
sortedEntries[sortedEntries.length - 1].key,
|
||||
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
|
||||
)
|
||||
}
|
||||
|
||||
console.groupEnd()
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async clearAll(): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||
const store = transaction.objectStore('heads')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.clear()
|
||||
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const headStorage = new HeadStorage()
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RenderResult } from '../rendering/batch-skin-renderer'
|
||||
import type { RawRenderResult } from '../rendering/batch-skin-renderer'
|
||||
|
||||
interface StoredPreview {
|
||||
forwards: Blob
|
||||
@@ -30,18 +30,15 @@ export class SkinPreviewStorage {
|
||||
})
|
||||
}
|
||||
|
||||
async store(key: string, result: RenderResult): Promise<void> {
|
||||
async store(key: string, result: RawRenderResult): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const forwardsBlob = await fetch(result.forwards).then((r) => r.blob())
|
||||
const backwardsBlob = await fetch(result.backwards).then((r) => r.blob())
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readwrite')
|
||||
const store = transaction.objectStore('previews')
|
||||
|
||||
const storedPreview: StoredPreview = {
|
||||
forwards: forwardsBlob,
|
||||
backwards: backwardsBlob,
|
||||
forwards: result.forwards,
|
||||
backwards: result.backwards,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
@@ -53,7 +50,7 @@ export class SkinPreviewStorage {
|
||||
})
|
||||
}
|
||||
|
||||
async retrieve(key: string): Promise<RenderResult | null> {
|
||||
async retrieve(key: string): Promise<RawRenderResult | null> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||
@@ -70,14 +67,56 @@ export class SkinPreviewStorage {
|
||||
return
|
||||
}
|
||||
|
||||
const forwards = URL.createObjectURL(result.forwards)
|
||||
const backwards = URL.createObjectURL(result.backwards)
|
||||
resolve({ forwards, backwards })
|
||||
resolve({ forwards: result.forwards, backwards: result.backwards })
|
||||
}
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async batchRetrieve(keys: string[]): Promise<Record<string, RawRenderResult | null>> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||
const store = transaction.objectStore('previews')
|
||||
const results: Record<string, RawRenderResult | null> = {}
|
||||
|
||||
return new Promise((resolve, _reject) => {
|
||||
let completedRequests = 0
|
||||
|
||||
if (keys.length === 0) {
|
||||
resolve(results)
|
||||
return
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const request = store.get(key)
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as StoredPreview | undefined
|
||||
|
||||
if (result) {
|
||||
results[key] = { forwards: result.forwards, backwards: result.backwards }
|
||||
} else {
|
||||
results[key] = null
|
||||
}
|
||||
|
||||
completedRequests++
|
||||
if (completedRequests === keys.length) {
|
||||
resolve(results)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
results[key] = null
|
||||
completedRequests++
|
||||
if (completedRequests === keys.length) {
|
||||
resolve(results)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
@@ -113,6 +152,67 @@ export class SkinPreviewStorage {
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async debugCalculateStorage(): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||
const store = transaction.objectStore('previews')
|
||||
|
||||
let totalSize = 0
|
||||
let count = 0
|
||||
const entries: Array<{ key: string; size: number }> = []
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.openCursor()
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||
|
||||
if (cursor) {
|
||||
const key = cursor.primaryKey as string
|
||||
const value = cursor.value as StoredPreview
|
||||
|
||||
const entrySize = value.forwards.size + value.backwards.size
|
||||
totalSize += entrySize
|
||||
count++
|
||||
|
||||
entries.push({
|
||||
key,
|
||||
size: entrySize,
|
||||
})
|
||||
|
||||
cursor.continue()
|
||||
} else {
|
||||
console.group('🗄️ Skin Preview Storage Debug Info')
|
||||
console.log(`Total entries: ${count}`)
|
||||
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
|
||||
console.log(
|
||||
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
|
||||
)
|
||||
|
||||
if (entries.length > 0) {
|
||||
const sortedEntries = entries.sort((a, b) => b.size - a.size)
|
||||
console.log(
|
||||
'Largest entry:',
|
||||
sortedEntries[0].key,
|
||||
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
|
||||
)
|
||||
console.log(
|
||||
'Smallest entry:',
|
||||
sortedEntries[sortedEntries.length - 1].key,
|
||||
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
|
||||
)
|
||||
}
|
||||
|
||||
console.groupEnd()
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const skinPreviewStorage = new SkinPreviewStorage()
|
||||
@@ -377,6 +377,12 @@
|
||||
"instance.worlds.hardcore": {
|
||||
"message": "Hardcore mode"
|
||||
},
|
||||
"instance.worlds.incompatible_server": {
|
||||
"message": "Server is incompatible"
|
||||
},
|
||||
"instance.worlds.no_contact": {
|
||||
"message": "Server couldn't be contacted"
|
||||
},
|
||||
"instance.worlds.no_quick_play": {
|
||||
"message": "You can only jump straight into worlds on Minecraft 1.20+"
|
||||
},
|
||||
|
||||
@@ -38,15 +38,11 @@ import {
|
||||
import { get as getSettings } from '@/helpers/settings.ts'
|
||||
import { get_default_user, login as login_flow, users } from '@/helpers/auth'
|
||||
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import { generateSkinPreviews, map } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import { generateSkinPreviews, skinBlobUrlMap } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import { handleSevereError } from '@/store/error'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import type AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
import { arrayBufferToBase64 } from '@modrinth/utils'
|
||||
import capeModelUrl from '@/assets/models/cape.gltf?url'
|
||||
import wideModelUrl from '@/assets/models/classic_player.gltf?url'
|
||||
import slimModelUrl from '@/assets/models/slim_player.gltf?url'
|
||||
|
||||
const editSkinModal = useTemplateRef('editSkinModal')
|
||||
const selectCapeModal = useTemplateRef('selectCapeModal')
|
||||
const uploadSkinModal = useTemplateRef('uploadSkinModal')
|
||||
@@ -219,7 +215,7 @@ async function loadCurrentUser() {
|
||||
|
||||
function getBakedSkinTextures(skin: Skin): RenderResult | undefined {
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
return map.get(key)
|
||||
return skinBlobUrlMap.get(key)
|
||||
}
|
||||
|
||||
async function login() {
|
||||
@@ -320,9 +316,6 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
|
||||
</h1>
|
||||
<div class="preview-container">
|
||||
<SkinPreviewRenderer
|
||||
:wide-model-src="wideModelUrl"
|
||||
:slim-model-src="slimModelUrl"
|
||||
:cape-model-src="capeModelUrl"
|
||||
:cape-src="capeTexture"
|
||||
:texture-src="skinTexture || ''"
|
||||
:variant="skinVariant"
|
||||
|
||||
@@ -483,7 +483,7 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
height: calc(100vh - 11rem);
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "theseus_gui"
|
||||
version = "0.10.1"
|
||||
version = "1.0.0-local" # The actual version is set by the theseus-build workflow on tagging
|
||||
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
|
||||
license = "GPL-3.0-only"
|
||||
repository = "https://github.com/modrinth/code/apps/app/"
|
||||
|
||||
@@ -18,5 +18,25 @@
|
||||
<string>A Minecraft mod wants to access your camera.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>A Minecraft mod wants to access your microphone.</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>asset.localhost</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>textures.minecraft.net</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -41,7 +41,7 @@
|
||||
]
|
||||
},
|
||||
"productName": "Modrinth App",
|
||||
"version": "0.10.1",
|
||||
"version": "../app-frontend/package.json",
|
||||
"mainBinaryName": "Modrinth App",
|
||||
"identifier": "ModrinthApp",
|
||||
"plugins": {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
FROM rust:1.88.0 AS build
|
||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||
|
||||
WORKDIR /usr/src/daedalus
|
||||
COPY . .
|
||||
@@ -10,11 +9,8 @@ FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates openssl \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN update-ca-certificates
|
||||
|
||||
COPY --from=build /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
|
||||
WORKDIR /daedalus_client
|
||||
|
||||
|
||||
@@ -19,8 +19,6 @@ From there, you can create the database and perform all database migrations with
|
||||
sqlx database setup
|
||||
```
|
||||
|
||||
Finally, if on Linux, you will need the OpenSSL library. On Debian-based systems, this involves the `pkg-config` and `libssl-dev` packages.
|
||||
|
||||
To enable labrinth to create a project, you need to add two things.
|
||||
|
||||
1. An entry in the `loaders` table.
|
||||
|
||||
@@ -38,9 +38,10 @@
|
||||
"@intercom/messenger-js-sdk": "^0.0.14",
|
||||
"@ltd/j-toml": "^1.38.0",
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/blog": "workspace:*",
|
||||
"@modrinth/moderation": "workspace:*",
|
||||
"@modrinth/ui": "workspace:*",
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"@modrinth/blog": "workspace:*",
|
||||
"@pinia/nuxt": "^0.5.1",
|
||||
"@types/three": "^0.172.0",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
@@ -58,6 +59,7 @@
|
||||
"markdown-it": "14.1.0",
|
||||
"pathe": "^1.1.2",
|
||||
"pinia": "^2.1.7",
|
||||
"prettier": "^3.6.2",
|
||||
"qrcode.vue": "^3.4.0",
|
||||
"semver": "^7.5.4",
|
||||
"three": "^0.172.0",
|
||||
|
||||
@@ -197,13 +197,13 @@
|
||||
}
|
||||
|
||||
> :where(
|
||||
input + *,
|
||||
.input-group + *,
|
||||
.textarea-wrapper + *,
|
||||
.chips + *,
|
||||
.resizable-textarea-wrapper + *,
|
||||
.input-div + *
|
||||
) {
|
||||
input + *,
|
||||
.input-group + *,
|
||||
.textarea-wrapper + *,
|
||||
.chips + *,
|
||||
.resizable-textarea-wrapper + *,
|
||||
.input-div + *
|
||||
) {
|
||||
&:not(:empty) {
|
||||
margin-block-start: var(--spacing-card-md);
|
||||
}
|
||||
|
||||
@@ -115,10 +115,12 @@ html {
|
||||
--shadow-inset-sm: inset 0px -1px 2px hsla(221, 39%, 11%, 0.15);
|
||||
|
||||
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
||||
--shadow-raised: 0.3px 0.5px 0.6px hsl(var(--shadow-color) / 0.15),
|
||||
--shadow-raised:
|
||||
0.3px 0.5px 0.6px hsl(var(--shadow-color) / 0.15),
|
||||
1px 2px 2.2px -1.7px hsl(var(--shadow-color) / 0.12),
|
||||
4.4px 8.8px 9.7px -3.4px hsl(var(--shadow-color) / 0.09);
|
||||
--shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
||||
--shadow-floating:
|
||||
hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
||||
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, hsla(0, 0%, 0%, 0.1) 0px 2px 4px -1px;
|
||||
|
||||
--shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px;
|
||||
@@ -150,8 +152,8 @@ html {
|
||||
rgba(255, 255, 255, 0.35) 0%,
|
||||
rgba(255, 255, 255, 0.2695) 100%
|
||||
);
|
||||
--landing-blob-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16),
|
||||
inset 2px 2px 64px rgba(255, 255, 255, 0.45);
|
||||
--landing-blob-shadow:
|
||||
2px 2px 12px rgba(0, 0, 0, 0.16), inset 2px 2px 64px rgba(255, 255, 255, 0.45);
|
||||
|
||||
--landing-card-bg: rgba(255, 255, 255, 0.8);
|
||||
--landing-card-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16);
|
||||
@@ -251,13 +253,15 @@ html {
|
||||
|
||||
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
||||
--shadow-raised: 0px -2px 4px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
||||
--shadow-floating:
|
||||
hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
||||
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
|
||||
|
||||
--shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px;
|
||||
|
||||
--landing-maze-bg: url("https://cdn.modrinth.com/landing-new/landing.webp");
|
||||
--landing-maze-gradient-bg: linear-gradient(0deg, #31375f 0%, rgba(8, 14, 55, 0) 100%),
|
||||
--landing-maze-gradient-bg:
|
||||
linear-gradient(0deg, #31375f 0%, rgba(8, 14, 55, 0) 100%),
|
||||
url("https://cdn.modrinth.com/landing-new/landing-lower.webp");
|
||||
--landing-maze-outer-bg: linear-gradient(180deg, #06060d 0%, #000000 100%);
|
||||
|
||||
@@ -284,7 +288,8 @@ html {
|
||||
rgba(44, 48, 79, 0.35) 0%,
|
||||
rgba(32, 35, 50, 0.2695) 100%
|
||||
);
|
||||
--landing-blob-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16), inset 2px 2px 64px rgba(57, 61, 94, 0.45);
|
||||
--landing-blob-shadow:
|
||||
2px 2px 12px rgba(0, 0, 0, 0.16), inset 2px 2px 64px rgba(57, 61, 94, 0.45);
|
||||
|
||||
--landing-card-bg: rgba(59, 63, 85, 0.15);
|
||||
--landing-card-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16);
|
||||
@@ -360,8 +365,9 @@ body {
|
||||
// Defaults
|
||||
background-color: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
--font-standard: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto,
|
||||
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
--font-standard:
|
||||
Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell,
|
||||
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
--mono-font: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||
font-family: var(--font-standard);
|
||||
font-size: 16px;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div
|
||||
class="vue-notification-group experimental-styles-within"
|
||||
:class="{ 'intercom-present': isIntercomPresent }"
|
||||
:class="{
|
||||
'intercom-present': isIntercomPresent,
|
||||
rightwards: moveNotificationsRight,
|
||||
}"
|
||||
>
|
||||
<transition-group name="notifs">
|
||||
<div
|
||||
@@ -82,6 +85,7 @@ import {
|
||||
CopyIcon,
|
||||
} from "@modrinth/assets";
|
||||
const notifications = useNotifications();
|
||||
const { isVisible: moveNotificationsRight } = useNotificationRightwards();
|
||||
|
||||
const isIntercomPresent = ref(false);
|
||||
|
||||
@@ -160,6 +164,15 @@ function copyToClipboard(notif) {
|
||||
bottom: 5rem;
|
||||
}
|
||||
|
||||
&.rightwards {
|
||||
right: unset !important;
|
||||
left: 1.5rem;
|
||||
|
||||
@media screen and (max-width: 500px) {
|
||||
left: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.vue-notification-wrapper {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Moderation shortcuts" :closable="true">
|
||||
<div>
|
||||
<div class="keybinds-sections">
|
||||
<div class="grid grid-cols-2 gap-x-12 gap-y-3">
|
||||
<div
|
||||
v-for="keybind in keybinds"
|
||||
:key="keybind.id"
|
||||
class="keybind-item flex items-center justify-between gap-4"
|
||||
:class="{
|
||||
'col-span-2': keybinds.length % 2 === 1 && keybinds[keybinds.length - 1] === keybind,
|
||||
}"
|
||||
>
|
||||
<span class="text-sm text-secondary">{{ keybind.description }}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<kbd
|
||||
v-for="(key, index) in parseKeybindDisplay(keybind.keybind)"
|
||||
:key="`${keybind.id}-key-${index}`"
|
||||
class="keybind-key"
|
||||
>
|
||||
{{ key }}
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import NewModal from "@modrinth/ui/src/components/modal/NewModal.vue";
|
||||
import { keybinds, type KeybindListener, normalizeKeybind } from "@modrinth/moderation";
|
||||
|
||||
const modal = ref<InstanceType<typeof NewModal>>();
|
||||
|
||||
function parseKeybindDisplay(keybind: KeybindListener["keybind"]): string[] {
|
||||
const keybinds = Array.isArray(keybind) ? keybind : [keybind];
|
||||
const normalized = keybinds[0];
|
||||
const def = normalizeKeybind(normalized);
|
||||
|
||||
const keys = [];
|
||||
|
||||
if (def.ctrl || def.meta) {
|
||||
keys.push(isMac() ? "CMD" : "CTRL");
|
||||
}
|
||||
if (def.shift) keys.push("SHIFT");
|
||||
if (def.alt) keys.push("ALT");
|
||||
|
||||
const mainKey = def.key
|
||||
.replace("ArrowLeft", "←")
|
||||
.replace("ArrowRight", "→")
|
||||
.replace("ArrowUp", "↑")
|
||||
.replace("ArrowDown", "↓")
|
||||
.replace("Enter", "↵")
|
||||
.replace("Space", "SPACE")
|
||||
.replace("Escape", "ESC")
|
||||
.toUpperCase();
|
||||
|
||||
keys.push(mainKey);
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
function isMac() {
|
||||
return navigator.platform.toUpperCase().indexOf("MAC") >= 0;
|
||||
}
|
||||
|
||||
function show(event?: MouseEvent) {
|
||||
modal.value?.show(event);
|
||||
}
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
hide,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.keybind-key {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 2rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background-color: var(--color-bg);
|
||||
border: 1px solid var(--color-divider);
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-contrast);
|
||||
|
||||
+ .keybind-key {
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.keybind-item {
|
||||
min-height: 2rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.keybinds-sections {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,422 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 v-if="modPackData" class="m-0 mb-2 text-lg font-extrabold">
|
||||
Modpack permissions ({{ Math.min(modPackData.length, currentIndex + 1) }} /
|
||||
{{ modPackData.length }})
|
||||
</h2>
|
||||
|
||||
<div v-if="!modPackData">Loading data...</div>
|
||||
|
||||
<div v-else-if="modPackData.length === 0">
|
||||
<p>All permissions obtained. You may skip this step!</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!modPackData[currentIndex]">
|
||||
<p>All permission checks complete!</p>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-if="modPackData[currentIndex].type === 'unknown'">
|
||||
<p>What is the approval type of {{ modPackData[currentIndex].file_name }}?</p>
|
||||
<div class="input-group">
|
||||
<ButtonStyled
|
||||
v-for="(option, index) in fileApprovalTypes"
|
||||
:key="index"
|
||||
:color="modPackData[currentIndex].status === option.id ? 'brand' : 'standard'"
|
||||
@click="setStatus(currentIndex, option.id)"
|
||||
>
|
||||
<button>
|
||||
{{ option.name }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<div v-if="modPackData[currentIndex].status !== 'unidentified'" class="flex flex-col gap-1">
|
||||
<label for="proof">
|
||||
<span class="label__title">Proof</span>
|
||||
</label>
|
||||
<input
|
||||
id="proof"
|
||||
v-model="(modPackData[currentIndex] as ModerationUnknownModpackItem).proof"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="Enter proof of status..."
|
||||
@input="persistAll()"
|
||||
/>
|
||||
<label for="link">
|
||||
<span class="label__title">Link</span>
|
||||
</label>
|
||||
<input
|
||||
id="link"
|
||||
v-model="(modPackData[currentIndex] as ModerationUnknownModpackItem).url"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="Enter link of project..."
|
||||
@input="persistAll()"
|
||||
/>
|
||||
<label for="title">
|
||||
<span class="label__title">Title</span>
|
||||
</label>
|
||||
<input
|
||||
id="title"
|
||||
v-model="(modPackData[currentIndex] as ModerationUnknownModpackItem).title"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
placeholder="Enter title of project..."
|
||||
@input="persistAll()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="modPackData[currentIndex].type === 'flame'">
|
||||
<p>
|
||||
What is the approval type of {{ modPackData[currentIndex].title }} (<a
|
||||
:href="modPackData[currentIndex].url"
|
||||
target="_blank"
|
||||
class="text-link"
|
||||
>{{ modPackData[currentIndex].url }}</a
|
||||
>)?
|
||||
</p>
|
||||
<div class="input-group">
|
||||
<ButtonStyled
|
||||
v-for="(option, index) in fileApprovalTypes"
|
||||
:key="index"
|
||||
:color="modPackData[currentIndex].status === option.id ? 'brand' : 'standard'"
|
||||
@click="setStatus(currentIndex, option.id)"
|
||||
>
|
||||
<button>
|
||||
{{ option.name }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="
|
||||
['unidentified', 'no', 'with-attribution'].includes(
|
||||
modPackData[currentIndex].status || '',
|
||||
)
|
||||
"
|
||||
>
|
||||
<p v-if="modPackData[currentIndex].status === 'unidentified'">
|
||||
Does this project provide identification and permission for
|
||||
<strong>{{ modPackData[currentIndex].file_name }}</strong
|
||||
>?
|
||||
</p>
|
||||
<p v-else-if="modPackData[currentIndex].status === 'with-attribution'">
|
||||
Does this project provide attribution for
|
||||
<strong>{{ modPackData[currentIndex].file_name }}</strong
|
||||
>?
|
||||
</p>
|
||||
<p v-else>
|
||||
Does this project provide proof of permission for
|
||||
<strong>{{ modPackData[currentIndex].file_name }}</strong
|
||||
>?
|
||||
</p>
|
||||
<div class="input-group">
|
||||
<ButtonStyled
|
||||
v-for="(option, index) in filePermissionTypes"
|
||||
:key="index"
|
||||
:color="modPackData[currentIndex].approved === option.id ? 'brand' : 'standard'"
|
||||
@click="setApproval(currentIndex, option.id)"
|
||||
>
|
||||
<button>
|
||||
{{ option.name }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex gap-2">
|
||||
<ButtonStyled>
|
||||
<button :disabled="currentIndex <= 0" @click="goToPrevious">
|
||||
<LeftArrowIcon aria-hidden="true" />
|
||||
Previous
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled v-if="modPackData && currentIndex < modPackData.length" color="blue">
|
||||
<button :disabled="!canGoNext" @click="goToNext">
|
||||
<RightArrowIcon aria-hidden="true" />
|
||||
{{ currentIndex + 1 >= modPackData.length ? "Complete" : "Next" }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LeftArrowIcon, RightArrowIcon } from "@modrinth/assets";
|
||||
import type {
|
||||
ModerationJudgements,
|
||||
ModerationModpackItem,
|
||||
ModerationModpackResponse,
|
||||
ModerationUnknownModpackItem,
|
||||
ModerationFlameModpackItem,
|
||||
ModerationModpackPermissionApprovalType,
|
||||
ModerationPermissionType,
|
||||
} from "@modrinth/utils";
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
|
||||
const props = defineProps<{
|
||||
projectId: string;
|
||||
modelValue?: ModerationJudgements;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
complete: [];
|
||||
"update:modelValue": [judgements: ModerationJudgements];
|
||||
}>();
|
||||
|
||||
const persistedModPackData = useLocalStorage<ModerationModpackItem[] | null>(
|
||||
`modpack-permissions-${props.projectId}`,
|
||||
null,
|
||||
{
|
||||
serializer: {
|
||||
read: (v: any) => (v ? JSON.parse(v) : null),
|
||||
write: (v: any) => JSON.stringify(v),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const persistedIndex = useLocalStorage<number>(`modpack-permissions-index-${props.projectId}`, 0);
|
||||
|
||||
const modPackData = ref<ModerationModpackItem[] | null>(null);
|
||||
const currentIndex = ref(0);
|
||||
|
||||
const fileApprovalTypes: ModerationModpackPermissionApprovalType[] = [
|
||||
{
|
||||
id: "yes",
|
||||
name: "Yes",
|
||||
},
|
||||
{
|
||||
id: "with-attribution-and-source",
|
||||
name: "With attribution and source",
|
||||
},
|
||||
{
|
||||
id: "with-attribution",
|
||||
name: "With attribution",
|
||||
},
|
||||
{
|
||||
id: "no",
|
||||
name: "No",
|
||||
},
|
||||
{
|
||||
id: "permanent-no",
|
||||
name: "Permanent no",
|
||||
},
|
||||
{
|
||||
id: "unidentified",
|
||||
name: "Unidentified",
|
||||
},
|
||||
];
|
||||
|
||||
const filePermissionTypes: ModerationPermissionType[] = [
|
||||
{ id: "yes", name: "Yes" },
|
||||
{ id: "no", name: "No" },
|
||||
];
|
||||
|
||||
function persistAll() {
|
||||
persistedModPackData.value = modPackData.value;
|
||||
persistedIndex.value = currentIndex.value;
|
||||
}
|
||||
|
||||
watch(
|
||||
modPackData,
|
||||
(newValue) => {
|
||||
persistedModPackData.value = newValue;
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(currentIndex, (newValue) => {
|
||||
persistedIndex.value = newValue;
|
||||
});
|
||||
|
||||
function loadPersistedData(): void {
|
||||
if (persistedModPackData.value) {
|
||||
modPackData.value = persistedModPackData.value;
|
||||
}
|
||||
currentIndex.value = persistedIndex.value;
|
||||
}
|
||||
|
||||
function clearPersistedData(): void {
|
||||
persistedModPackData.value = null;
|
||||
persistedIndex.value = 0;
|
||||
}
|
||||
|
||||
async function fetchModPackData(): Promise<void> {
|
||||
try {
|
||||
const data = (await useBaseFetch(`moderation/project/${props.projectId}`, {
|
||||
internal: true,
|
||||
})) as ModerationModpackResponse;
|
||||
const sortedData: ModerationModpackItem[] = [
|
||||
...Object.entries(data.unknown_files || {})
|
||||
.map(
|
||||
([sha1, fileName]): ModerationUnknownModpackItem => ({
|
||||
sha1,
|
||||
file_name: fileName,
|
||||
type: "unknown",
|
||||
status: null,
|
||||
approved: null,
|
||||
proof: "",
|
||||
url: "",
|
||||
title: "",
|
||||
}),
|
||||
)
|
||||
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
||||
...Object.entries(data.flame_files || {})
|
||||
.map(
|
||||
([sha1, info]): ModerationFlameModpackItem => ({
|
||||
sha1,
|
||||
file_name: info.file_name,
|
||||
type: "flame",
|
||||
status: null,
|
||||
approved: null,
|
||||
id: info.id,
|
||||
title: info.title || info.file_name,
|
||||
url: info.url || `https://www.curseforge.com/minecraft/mc-mods/${info.id}`,
|
||||
}),
|
||||
)
|
||||
.sort((a, b) => a.file_name.localeCompare(b.file_name)),
|
||||
];
|
||||
|
||||
if (modPackData.value) {
|
||||
const existingMap = new Map(modPackData.value.map((item) => [item.sha1, item]));
|
||||
|
||||
sortedData.forEach((item) => {
|
||||
const existing = existingMap.get(item.sha1);
|
||||
if (existing) {
|
||||
Object.assign(item, {
|
||||
status: existing.status,
|
||||
approved: existing.approved,
|
||||
...(item.type === "unknown" && {
|
||||
proof: (existing as ModerationUnknownModpackItem).proof || "",
|
||||
url: (existing as ModerationUnknownModpackItem).url || "",
|
||||
title: (existing as ModerationUnknownModpackItem).title || "",
|
||||
}),
|
||||
...(item.type === "flame" && {
|
||||
url: (existing as ModerationFlameModpackItem).url || item.url,
|
||||
title: (existing as ModerationFlameModpackItem).title || item.title,
|
||||
}),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
modPackData.value = sortedData;
|
||||
persistAll();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch modpack data:", error);
|
||||
modPackData.value = [];
|
||||
persistAll();
|
||||
}
|
||||
}
|
||||
|
||||
function goToPrevious(): void {
|
||||
if (currentIndex.value > 0) {
|
||||
currentIndex.value--;
|
||||
persistAll();
|
||||
}
|
||||
}
|
||||
|
||||
function goToNext(): void {
|
||||
if (modPackData.value && currentIndex.value < modPackData.value.length) {
|
||||
currentIndex.value++;
|
||||
|
||||
if (currentIndex.value >= modPackData.value.length) {
|
||||
const judgements = getJudgements();
|
||||
emit("update:modelValue", judgements);
|
||||
emit("complete");
|
||||
clearPersistedData();
|
||||
} else {
|
||||
persistAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(index: number, status: ModerationModpackPermissionApprovalType["id"]): void {
|
||||
if (modPackData.value && modPackData.value[index]) {
|
||||
modPackData.value[index].status = status;
|
||||
modPackData.value[index].approved = null;
|
||||
persistAll();
|
||||
emit("update:modelValue", getJudgements());
|
||||
}
|
||||
}
|
||||
|
||||
function setApproval(index: number, approved: ModerationPermissionType["id"]): void {
|
||||
if (modPackData.value && modPackData.value[index]) {
|
||||
modPackData.value[index].approved = approved;
|
||||
persistAll();
|
||||
emit("update:modelValue", getJudgements());
|
||||
}
|
||||
}
|
||||
|
||||
const canGoNext = computed(() => {
|
||||
if (!modPackData.value || !modPackData.value[currentIndex.value]) return false;
|
||||
const current = modPackData.value[currentIndex.value];
|
||||
return current.status !== null;
|
||||
});
|
||||
|
||||
function getJudgements(): ModerationJudgements {
|
||||
if (!modPackData.value) return {};
|
||||
|
||||
const judgements: ModerationJudgements = {};
|
||||
|
||||
modPackData.value.forEach((item) => {
|
||||
if (item.type === "flame") {
|
||||
judgements[item.sha1] = {
|
||||
type: "flame",
|
||||
id: item.id,
|
||||
status: item.status,
|
||||
link: item.url,
|
||||
title: item.title,
|
||||
file_name: item.file_name,
|
||||
};
|
||||
} else if (item.type === "unknown") {
|
||||
judgements[item.sha1] = {
|
||||
type: "unknown",
|
||||
status: item.status,
|
||||
proof: item.proof,
|
||||
link: item.url,
|
||||
title: item.title,
|
||||
file_name: item.file_name,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return judgements;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadPersistedData();
|
||||
if (!modPackData.value) {
|
||||
fetchModPackData();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.projectId,
|
||||
() => {
|
||||
clearPersistedData();
|
||||
loadPersistedData();
|
||||
if (!modPackData.value) {
|
||||
fetchModPackData();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.modpack-buttons {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -172,6 +172,7 @@ const flags = useFeatureFlags();
|
||||
|
||||
.markdown-body {
|
||||
grid-area: body;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.reporter-info {
|
||||
|
||||
@@ -31,9 +31,9 @@
|
||||
class="flex cursor-pointer items-center gap-1 bg-transparent p-0"
|
||||
@click="
|
||||
versionFilter &&
|
||||
(unlockFilterAccordion.isOpen
|
||||
? unlockFilterAccordion.close()
|
||||
: unlockFilterAccordion.open())
|
||||
(unlockFilterAccordion.isOpen
|
||||
? unlockFilterAccordion.close()
|
||||
: unlockFilterAccordion.open())
|
||||
"
|
||||
>
|
||||
<TagItem
|
||||
|
||||
@@ -102,7 +102,7 @@ export class ModrinthServer {
|
||||
try {
|
||||
const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, {
|
||||
override: auth,
|
||||
retry: false,
|
||||
retry: 1, // Reduce retries for optional resources
|
||||
});
|
||||
|
||||
if (fileData instanceof Blob && import.meta.client) {
|
||||
@@ -124,8 +124,14 @@ export class ModrinthServer {
|
||||
return dataURL;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServerError && error.statusCode === 404) {
|
||||
if (iconUrl) {
|
||||
if (error instanceof ModrinthServerError) {
|
||||
if (error.statusCode && error.statusCode >= 500) {
|
||||
console.debug("Service unavailable, skipping icon processing");
|
||||
sharedImage.value = undefined;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (error.statusCode === 404 && iconUrl) {
|
||||
try {
|
||||
const response = await fetch(iconUrl);
|
||||
if (!response.ok) throw new Error("Failed to fetch icon");
|
||||
@@ -187,6 +193,44 @@ export class ModrinthServer {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async testNodeReachability(): Promise<boolean> {
|
||||
if (!this.general?.node?.instance) {
|
||||
console.warn("No node instance available for ping test");
|
||||
return false;
|
||||
}
|
||||
|
||||
const wsUrl = `wss://${this.general.node.instance}/pingtest`;
|
||||
|
||||
try {
|
||||
return await new Promise((resolve) => {
|
||||
const socket = new WebSocket(wsUrl);
|
||||
const timeout = setTimeout(() => {
|
||||
socket.close();
|
||||
resolve(false);
|
||||
}, 5000);
|
||||
|
||||
socket.onopen = () => {
|
||||
clearTimeout(timeout);
|
||||
socket.send(performance.now().toString());
|
||||
};
|
||||
|
||||
socket.onmessage = () => {
|
||||
clearTimeout(timeout);
|
||||
socket.close();
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
socket.onerror = () => {
|
||||
clearTimeout(timeout);
|
||||
resolve(false);
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Failed to ping node ${wsUrl}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async refresh(
|
||||
modules: ModuleName[] = [],
|
||||
options?: {
|
||||
@@ -200,6 +244,8 @@ export class ModrinthServer {
|
||||
: (["general", "content", "backups", "network", "startup", "ws", "fs"] as ModuleName[]);
|
||||
|
||||
for (const module of modulesToRefresh) {
|
||||
this.errors[module] = undefined;
|
||||
|
||||
try {
|
||||
switch (module) {
|
||||
case "general": {
|
||||
@@ -250,7 +296,7 @@ export class ModrinthServer {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (error.statusCode === 503) {
|
||||
if (error.statusCode && error.statusCode >= 500) {
|
||||
console.debug(`Temporary ${module} unavailable:`, error.message);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -22,26 +22,49 @@ export class FSModule extends ServerModule {
|
||||
this.opsQueuedForModification = [];
|
||||
}
|
||||
|
||||
private async retryWithAuth<T>(requestFn: () => Promise<T>): Promise<T> {
|
||||
private async retryWithAuth<T>(
|
||||
requestFn: () => Promise<T>,
|
||||
ignoreFailure: boolean = false,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await requestFn();
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServerError && error.statusCode === 401) {
|
||||
console.debug("Auth failed, refreshing JWT and retrying");
|
||||
await this.fetch(); // Refresh auth
|
||||
return await requestFn();
|
||||
}
|
||||
|
||||
const available = await this.server.testNodeReachability();
|
||||
if (!available && !ignoreFailure) {
|
||||
this.server.moduleErrors.general = {
|
||||
error: new ModrinthServerError(
|
||||
"Unable to reach node. FS operation failed and subsequent ping test failed.",
|
||||
500,
|
||||
error as Error,
|
||||
"fs",
|
||||
),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
listDirContents(path: string, page: number, pageSize: number): Promise<DirectoryResponse> {
|
||||
listDirContents(
|
||||
path: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
ignoreFailure: boolean = false,
|
||||
): Promise<DirectoryResponse> {
|
||||
return this.retryWithAuth(async () => {
|
||||
const encodedPath = encodeURIComponent(path);
|
||||
return await useServersFetch(`/list?path=${encodedPath}&page=${page}&page_size=${pageSize}`, {
|
||||
override: this.auth,
|
||||
retry: false,
|
||||
});
|
||||
});
|
||||
}, ignoreFailure);
|
||||
}
|
||||
|
||||
createFileOrFolder(path: string, type: "file" | "directory"): Promise<void> {
|
||||
@@ -150,7 +173,7 @@ export class FSModule extends ServerModule {
|
||||
});
|
||||
}
|
||||
|
||||
downloadFile(path: string, raw?: boolean): Promise<any> {
|
||||
downloadFile(path: string, raw: boolean = false, ignoreFailure: boolean = false): Promise<any> {
|
||||
return this.retryWithAuth(async () => {
|
||||
const encodedPath = encodeURIComponent(path);
|
||||
const fileData = await useServersFetch(`/download?path=${encodedPath}`, {
|
||||
@@ -161,7 +184,7 @@ export class FSModule extends ServerModule {
|
||||
return raw ? fileData : await fileData.text();
|
||||
}
|
||||
return fileData;
|
||||
});
|
||||
}, ignoreFailure);
|
||||
}
|
||||
|
||||
extractFile(
|
||||
|
||||
@@ -46,13 +46,18 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
|
||||
data.image = (await this.server.processImage(data.project?.icon_url)) ?? undefined;
|
||||
}
|
||||
|
||||
const motd = await this.getMotd();
|
||||
if (motd === "A Minecraft Server") {
|
||||
await this.setMotd(
|
||||
`§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`,
|
||||
);
|
||||
try {
|
||||
const motd = await this.getMotd();
|
||||
if (motd === "A Minecraft Server") {
|
||||
await this.setMotd(
|
||||
`§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`,
|
||||
);
|
||||
}
|
||||
data.motd = motd;
|
||||
} catch {
|
||||
console.error("[Modrinth Servers] [General] Failed to fetch MOTD.");
|
||||
data.motd = undefined;
|
||||
}
|
||||
data.motd = motd;
|
||||
|
||||
// Copy data to this module
|
||||
Object.assign(this, data);
|
||||
@@ -178,7 +183,7 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
|
||||
|
||||
async getMotd(): Promise<string | undefined> {
|
||||
try {
|
||||
const props = await this.server.fs.downloadFile("/server.properties");
|
||||
const props = await this.server.fs.downloadFile("/server.properties", false, true);
|
||||
if (props) {
|
||||
const lines = props.split("\n");
|
||||
for (const line of lines) {
|
||||
|
||||
@@ -42,6 +42,23 @@ export async function useServersFetch<T>(
|
||||
retry = method === "GET" ? 3 : 0,
|
||||
} = options;
|
||||
|
||||
const circuitBreakerKey = `${module || "default"}_${path}`;
|
||||
const failureCount = useState<number>(`fetch_failures_${circuitBreakerKey}`, () => 0);
|
||||
const lastFailureTime = useState<number>(`last_failure_${circuitBreakerKey}`, () => 0);
|
||||
|
||||
const now = Date.now();
|
||||
if (failureCount.value >= 3 && now - lastFailureTime.value < 30000) {
|
||||
const error = new ModrinthServersFetchError(
|
||||
"[Modrinth Servers] Circuit breaker open - too many recent failures",
|
||||
503,
|
||||
);
|
||||
throw new ModrinthServerError("Service temporarily unavailable", 503, error, module);
|
||||
}
|
||||
|
||||
if (now - lastFailureTime.value > 30000) {
|
||||
failureCount.value = 0;
|
||||
}
|
||||
|
||||
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
|
||||
/\/$/,
|
||||
"",
|
||||
@@ -69,6 +86,7 @@ export async function useServersFetch<T>(
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"User-Agent": "Modrinth/1.0 (https://modrinth.com)",
|
||||
"X-Archon-Request": "true",
|
||||
Vary: "Accept, Origin",
|
||||
};
|
||||
|
||||
@@ -94,10 +112,12 @@ export async function useServersFetch<T>(
|
||||
const response = await $fetch<T>(fullUrl, {
|
||||
method,
|
||||
headers,
|
||||
body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined,
|
||||
body:
|
||||
body && contentType === "application/json" ? JSON.stringify(body) : (body ?? undefined),
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
failureCount.value = 0;
|
||||
return response;
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
@@ -107,6 +127,11 @@ export async function useServersFetch<T>(
|
||||
const statusCode = error.response?.status;
|
||||
const statusText = error.response?.statusText || "Unknown error";
|
||||
|
||||
if (statusCode && statusCode >= 500) {
|
||||
failureCount.value++;
|
||||
lastFailureTime.value = now;
|
||||
}
|
||||
|
||||
let v1Error: V1ErrorInfo | undefined;
|
||||
if (error.data?.error && error.data?.description) {
|
||||
v1Error = {
|
||||
@@ -134,9 +159,11 @@ export async function useServersFetch<T>(
|
||||
? errorMessages[statusCode]
|
||||
: `HTTP Error: ${statusCode || "unknown"} ${statusText}`;
|
||||
|
||||
const isRetryable = statusCode ? [408, 429, 500, 502, 504].includes(statusCode) : true;
|
||||
const isRetryable = statusCode ? [408, 429].includes(statusCode) : false;
|
||||
const is5xxRetryable =
|
||||
statusCode && statusCode >= 500 && statusCode < 600 && method === "GET" && attempts === 1;
|
||||
|
||||
if (!isRetryable || attempts >= maxAttempts) {
|
||||
if (!(isRetryable || is5xxRetryable) || attempts >= maxAttempts) {
|
||||
console.error("Fetch error:", error);
|
||||
|
||||
const fetchError = new ModrinthServersFetchError(
|
||||
@@ -147,7 +174,8 @@ export async function useServersFetch<T>(
|
||||
throw new ModrinthServerError(error.message, statusCode, fetchError, module, v1Error);
|
||||
}
|
||||
|
||||
const delay = Math.min(1000 * Math.pow(2, attempts - 1) + Math.random() * 1000, 10000);
|
||||
const baseDelay = statusCode && statusCode >= 500 ? 5000 : 1000;
|
||||
const delay = Math.min(baseDelay * Math.pow(2, attempts - 1) + Math.random() * 1000, 15000);
|
||||
console.warn(`Retrying request in ${delay}ms (attempt ${attempts}/${maxAttempts - 1})`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
continue;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export const useNotificationRightwards = () => {
|
||||
const isVisible = useState("moderation-checklist-notifications", () => false);
|
||||
|
||||
const setVisible = (visible: boolean) => {
|
||||
isVisible.value = visible;
|
||||
};
|
||||
|
||||
return {
|
||||
isVisible: readonly(isVisible),
|
||||
setVisible,
|
||||
};
|
||||
};
|
||||
@@ -700,7 +700,6 @@ import {
|
||||
PackageOpenIcon,
|
||||
DiscordIcon,
|
||||
BlueskyIcon,
|
||||
TumblrIcon,
|
||||
TwitterIcon,
|
||||
MastodonIcon,
|
||||
GithubIcon,
|
||||
@@ -1185,13 +1184,6 @@ const socialLinks = [
|
||||
icon: MastodonIcon,
|
||||
rel: "me",
|
||||
},
|
||||
{
|
||||
label: formatMessage(
|
||||
defineMessage({ id: "layout.footer.social.tumblr", defaultMessage: "Tumblr" }),
|
||||
),
|
||||
href: "https://tumblr.com/modrinth",
|
||||
icon: TumblrIcon,
|
||||
},
|
||||
{
|
||||
label: formatMessage(defineMessage({ id: "layout.footer.social.x", defaultMessage: "X" })),
|
||||
href: "https://x.com/modrinth",
|
||||
@@ -1346,6 +1338,15 @@ const footerLinks = [
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
href: "/legal/copyright",
|
||||
label: formatMessage(
|
||||
defineMessage({
|
||||
id: "layout.footer.legal.copyright-policy",
|
||||
defaultMessage: "Copyright Policy and DMCA",
|
||||
}),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -383,15 +383,15 @@
|
||||
"layout.footer.about": {
|
||||
"message": "About"
|
||||
},
|
||||
"layout.footer.about.news": {
|
||||
"message": "News"
|
||||
},
|
||||
"layout.footer.about.careers": {
|
||||
"message": "Careers"
|
||||
},
|
||||
"layout.footer.about.changelog": {
|
||||
"message": "Changelog"
|
||||
},
|
||||
"layout.footer.about.news": {
|
||||
"message": "News"
|
||||
},
|
||||
"layout.footer.about.rewards-program": {
|
||||
"message": "Rewards Program"
|
||||
},
|
||||
@@ -404,6 +404,9 @@
|
||||
"layout.footer.legal-disclaimer": {
|
||||
"message": "NOT AN OFFICIAL MINECRAFT SERVICE. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT."
|
||||
},
|
||||
"layout.footer.legal.copyright-policy": {
|
||||
"message": "Copyright Policy and DMCA"
|
||||
},
|
||||
"layout.footer.legal.privacy-policy": {
|
||||
"message": "Privacy Policy"
|
||||
},
|
||||
@@ -458,9 +461,6 @@
|
||||
"layout.footer.social.mastodon": {
|
||||
"message": "Mastodon"
|
||||
},
|
||||
"layout.footer.social.tumblr": {
|
||||
"message": "Tumblr"
|
||||
},
|
||||
"layout.footer.social.x": {
|
||||
"message": "X"
|
||||
},
|
||||
|
||||
@@ -29,12 +29,11 @@
|
||||
class="settings-header__icon"
|
||||
/>
|
||||
<div class="settings-header__text">
|
||||
<h1 class="wrap-as-needed">
|
||||
{{ project.title }}
|
||||
</h1>
|
||||
<h1 class="wrap-as-needed">{{ project.title }}</h1>
|
||||
<ProjectStatusBadge :status="project.status" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Project settings</h2>
|
||||
<NavStack>
|
||||
<NavStackItem
|
||||
@@ -111,6 +110,7 @@
|
||||
</NavStack>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="normal-page__content">
|
||||
<ProjectMemberHeader
|
||||
v-if="currentMember"
|
||||
@@ -145,6 +145,7 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="experimental-styles-within">
|
||||
<NewModal ref="settingsModal">
|
||||
<template #title>
|
||||
@@ -174,9 +175,11 @@
|
||||
<div
|
||||
class="animation-ring-3 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-40"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="animation-ring-2 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-60"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="animation-ring-1 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight"
|
||||
>
|
||||
@@ -219,8 +222,7 @@
|
||||
:href="`modrinth://mod/${project.slug}`"
|
||||
@click="() => installWithApp()"
|
||||
>
|
||||
<ModrinthIcon aria-hidden="true" />
|
||||
Install with Modrinth App
|
||||
<ModrinthIcon aria-hidden="true" /> Install with Modrinth App
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
@@ -240,6 +242,7 @@
|
||||
<div class="flex h-[2px] w-full rounded-2xl bg-button-bg"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mx-auto flex w-fit flex-col gap-2">
|
||||
<ButtonStyled v-if="project.game_versions.length === 1">
|
||||
<div class="disabled button-like">
|
||||
@@ -327,8 +330,7 @@
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ gameVersion }}
|
||||
<CheckIcon v-if="userSelectedGameVersion === gameVersion" />
|
||||
{{ gameVersion }} <CheckIcon v-if="userSelectedGameVersion === gameVersion" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</ScrollablePanel>
|
||||
@@ -419,7 +421,6 @@
|
||||
</ScrollablePanel>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
<AutomaticAccordion div class="flex flex-col gap-2">
|
||||
<VersionSummary
|
||||
v-if="filteredRelease"
|
||||
@@ -470,10 +471,14 @@
|
||||
class="new-page sidebar"
|
||||
:class="{
|
||||
'alt-layout': cosmetics.leftContentLayout,
|
||||
'ultimate-sidebar':
|
||||
'checklist-open':
|
||||
showModerationChecklist &&
|
||||
!collapsedModerationChecklist &&
|
||||
!flags.alwaysShowChecklistAsPopup,
|
||||
'checklist-collapsed':
|
||||
showModerationChecklist &&
|
||||
collapsedModerationChecklist &&
|
||||
!flags.alwaysShowChecklistAsPopup,
|
||||
}"
|
||||
>
|
||||
<div class="normal-page__header relative my-4">
|
||||
@@ -485,11 +490,11 @@
|
||||
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
|
||||
>
|
||||
<button @click="(event) => downloadModal.show(event)">
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
Download
|
||||
<DownloadIcon aria-hidden="true" /> Download
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<div class="contents sm:hidden">
|
||||
<ButtonStyled
|
||||
size="large"
|
||||
@@ -554,9 +559,11 @@
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
|
||||
<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
|
||||
Modrinth Servers is the easiest way to play with your friends without hassle!
|
||||
</p>
|
||||
|
||||
<p class="m-0 text-wrap text-sm font-bold text-primary">
|
||||
Starting at $5<span class="text-xs"> / month</span>
|
||||
</p>
|
||||
@@ -621,6 +628,7 @@
|
||||
{{ option.name }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div v-else class="menu-text">
|
||||
<p class="popout-text">No collections found.</p>
|
||||
</div>
|
||||
@@ -628,8 +636,7 @@
|
||||
class="btn collection-button"
|
||||
@click="(event) => $refs.modal_collection.show(event)"
|
||||
>
|
||||
<PlusIcon aria-hidden="true" />
|
||||
Create new collection
|
||||
<PlusIcon aria-hidden="true" /> Create new collection
|
||||
</button>
|
||||
</template>
|
||||
</PopoutMenu>
|
||||
@@ -712,25 +719,14 @@
|
||||
:dropdown-id="`${baseId}-more-options`"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #analytics>
|
||||
<ChartIcon aria-hidden="true" />
|
||||
Analytics
|
||||
</template>
|
||||
<template #analytics> <ChartIcon aria-hidden="true" /> Analytics </template>
|
||||
<template #moderation-checklist>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
Review project
|
||||
</template>
|
||||
<template #report>
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Report
|
||||
</template>
|
||||
<template #copy-id>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
Copy ID
|
||||
<ScaleIcon aria-hidden="true" /> Review project
|
||||
</template>
|
||||
<template #report> <ReportIcon aria-hidden="true" /> Report </template>
|
||||
<template #copy-id> <ClipboardCopyIcon aria-hidden="true" /> Copy ID </template>
|
||||
<template #copy-permalink>
|
||||
<ClipboardCopyIcon aria-hidden="true" />
|
||||
Copy permanent link
|
||||
<ClipboardCopyIcon aria-hidden="true" /> Copy permanent link
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
@@ -756,6 +752,7 @@
|
||||
updates unless the author decides to unarchive the project.
|
||||
</MessageBanner>
|
||||
</div>
|
||||
|
||||
<div class="normal-page__sidebar">
|
||||
<ProjectSidebarCompatibility
|
||||
:project="project"
|
||||
@@ -785,6 +782,7 @@
|
||||
/>
|
||||
<div class="card flex-card experimental-styles-within">
|
||||
<h2>{{ formatMessage(detailsMessages.title) }}</h2>
|
||||
|
||||
<div class="details-list">
|
||||
<div class="details-list__item">
|
||||
<BookTextIcon aria-hidden="true" />
|
||||
@@ -813,53 +811,48 @@
|
||||
<span v-else>{{ licenseIdDisplay }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="project.approved"
|
||||
v-tooltip="$dayjs(project.approved).format('MMMM D, YYYY [at] h:mm A')"
|
||||
class="details-list__item"
|
||||
>
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{ formatMessage(detailsMessages.published, { date: publishedDate }) }}
|
||||
</div>
|
||||
<div>{{ formatMessage(detailsMessages.published, { date: publishedDate }) }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
|
||||
class="details-list__item"
|
||||
>
|
||||
<CalendarIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{ formatMessage(detailsMessages.created, { date: createdDate }) }}
|
||||
</div>
|
||||
<div>{{ formatMessage(detailsMessages.created, { date: createdDate }) }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="project.status === 'processing' && project.queued"
|
||||
v-tooltip="$dayjs(project.queued).format('MMMM D, YYYY [at] h:mm A')"
|
||||
class="details-list__item"
|
||||
>
|
||||
<ScaleIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{ formatMessage(detailsMessages.submitted, { date: submittedDate }) }}
|
||||
</div>
|
||||
<div>{{ formatMessage(detailsMessages.submitted, { date: submittedDate }) }}</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="versions.length > 0 && project.updated"
|
||||
v-tooltip="$dayjs(project.updated).format('MMMM D, YYYY [at] h:mm A')"
|
||||
class="details-list__item"
|
||||
>
|
||||
<VersionIcon aria-hidden="true" />
|
||||
<div>
|
||||
{{ formatMessage(detailsMessages.updated, { date: updatedDate }) }}
|
||||
</div>
|
||||
<div>{{ formatMessage(detailsMessages.updated, { date: updatedDate }) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="normal-page__content">
|
||||
<div class="overflow-x-auto">
|
||||
<NavTabs :links="navLinks" class="mb-4" />
|
||||
</div>
|
||||
<div class="overflow-x-auto"><NavTabs :links="navLinks" class="mb-4" /></div>
|
||||
<NuxtPage
|
||||
v-model:project="project"
|
||||
v-model:versions="versions"
|
||||
@@ -877,8 +870,10 @@
|
||||
@delete-version="deleteVersion"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="normal-page__ultimate-sidebar">
|
||||
<ModerationChecklist
|
||||
<!-- Uncomment this to enable the old moderation checklist. -->
|
||||
<!-- <ModerationChecklist
|
||||
v-if="auth.user && tags.staffRoles.includes(auth.user.role) && showModerationChecklist"
|
||||
:project="project"
|
||||
:future-projects="futureProjects"
|
||||
@@ -886,11 +881,25 @@
|
||||
:collapsed="collapsedModerationChecklist"
|
||||
@exit="showModerationChecklist = false"
|
||||
@toggle-collapsed="collapsedModerationChecklist = !collapsedModerationChecklist"
|
||||
/>
|
||||
/> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="auth.user && tags.staffRoles.includes(auth.user.role) && showModerationChecklist"
|
||||
class="moderation-checklist"
|
||||
>
|
||||
<NewModerationChecklist
|
||||
:project="project"
|
||||
:future-project-ids="futureProjectIds"
|
||||
:collapsed="collapsedModerationChecklist"
|
||||
@exit="showModerationChecklist = false"
|
||||
@toggle-collapsed="collapsedModerationChecklist = !collapsedModerationChecklist"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
BookmarkIcon,
|
||||
@@ -950,16 +959,16 @@ import {
|
||||
isUnderReview,
|
||||
renderString,
|
||||
} from "@modrinth/utils";
|
||||
import { navigateTo } from "#app";
|
||||
import dayjs from "dayjs";
|
||||
import { Tooltip } from "floating-vue";
|
||||
import { useLocalStorage } from "@vueuse/core";
|
||||
import { navigateTo } from "#app";
|
||||
import Accordion from "~/components/ui/Accordion.vue";
|
||||
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
|
||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
|
||||
import MessageBanner from "~/components/ui/MessageBanner.vue";
|
||||
import ModerationChecklist from "~/components/ui/ModerationChecklist.vue";
|
||||
import NavStack from "~/components/ui/NavStack.vue";
|
||||
import NavStackItem from "~/components/ui/NavStackItem.vue";
|
||||
import NavTabs from "~/components/ui/NavTabs.vue";
|
||||
@@ -967,6 +976,7 @@ import ProjectMemberHeader from "~/components/ui/ProjectMemberHeader.vue";
|
||||
import { userCollectProject } from "~/composables/user.js";
|
||||
import { reportProject } from "~/utils/report-helpers.ts";
|
||||
import { saveFeatureFlags } from "~/composables/featureFlags.ts";
|
||||
import NewModerationChecklist from "~/components/ui/moderation/NewModerationChecklist.vue";
|
||||
|
||||
const data = useNuxtApp();
|
||||
const route = useNativeRoute();
|
||||
@@ -980,6 +990,7 @@ const flags = useFeatureFlags();
|
||||
const cosmetics = useCosmetics();
|
||||
|
||||
const { formatMessage } = useVIntl();
|
||||
const { setVisible } = useNotificationRightwards();
|
||||
|
||||
const settingsModal = ref();
|
||||
const downloadModal = ref();
|
||||
@@ -1551,12 +1562,28 @@ async function copyPermalink() {
|
||||
|
||||
const collapsedChecklist = ref(false);
|
||||
|
||||
const showModerationChecklist = ref(false);
|
||||
const collapsedModerationChecklist = ref(false);
|
||||
const futureProjects = ref([]);
|
||||
const showModerationChecklist = useLocalStorage(
|
||||
`show-moderation-checklist-${project.value.id}`,
|
||||
false,
|
||||
);
|
||||
const collapsedModerationChecklist = useLocalStorage("collapsed-moderation-checklist", false);
|
||||
|
||||
const futureProjectIds = useLocalStorage("moderation-future-projects", []);
|
||||
|
||||
watch(futureProjectIds, (newValue) => {
|
||||
console.log("Future project IDs updated:", newValue);
|
||||
});
|
||||
|
||||
watch(
|
||||
showModerationChecklist,
|
||||
(newValue) => {
|
||||
setVisible(newValue);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
if (import.meta.client && history && history.state && history.state.showChecklist) {
|
||||
showModerationChecklist.value = true;
|
||||
futureProjects.value = history.state.projects;
|
||||
}
|
||||
|
||||
function closeDownloadModal(event) {
|
||||
@@ -1626,6 +1653,7 @@ const navLinks = computed(() => {
|
||||
];
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.settings-header {
|
||||
display: flex;
|
||||
@@ -1781,4 +1809,16 @@ const navLinks = computed(() => {
|
||||
left: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.moderation-checklist {
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
right: 1rem;
|
||||
overflow-y: auto;
|
||||
z-index: 50;
|
||||
|
||||
> div {
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -705,9 +705,9 @@ export default defineNuxtComponent({
|
||||
}
|
||||
|
||||
.gallery-body {
|
||||
flex-grow: 1;
|
||||
width: calc(100% - 2 * var(--spacing-card-md));
|
||||
padding: var(--spacing-card-sm) var(--spacing-card-md);
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
.gallery-info {
|
||||
h2 {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
TrashIcon,
|
||||
SearchIcon,
|
||||
@@ -17,76 +17,134 @@ import LatestNewsRow from "~/components/ui/news/LatestNewsRow.vue";
|
||||
|
||||
import { homePageProjects } from "~/generated/state.json";
|
||||
|
||||
const os = ref(null);
|
||||
const downloadWindows = ref(null);
|
||||
const downloadLinux = ref(null);
|
||||
const downloadSection = ref(null);
|
||||
const windowsLink = ref(null);
|
||||
const linuxLinks = {
|
||||
appImage: null,
|
||||
deb: null,
|
||||
rpm: null,
|
||||
thirdParty: "https://support.modrinth.com/en/articles/9298760",
|
||||
};
|
||||
const macLinks = {
|
||||
universal: null,
|
||||
};
|
||||
interface LauncherPlatform {
|
||||
install_urls: string[];
|
||||
}
|
||||
|
||||
let downloadLauncher;
|
||||
interface LauncherUpdates {
|
||||
platforms: {
|
||||
"darwin-aarch64": LauncherPlatform;
|
||||
"windows-x86_64": LauncherPlatform;
|
||||
"linux-x86_64": LauncherPlatform;
|
||||
};
|
||||
}
|
||||
|
||||
type OSType = "Mac" | "Windows" | "Linux" | null;
|
||||
|
||||
const downloadWindows = ref<HTMLAnchorElement | null>(null);
|
||||
const downloadLinux = ref<HTMLAnchorElement | null>(null);
|
||||
const downloadSection = ref<HTMLElement | null>(null);
|
||||
const windowsLink = ref<string | null>(null);
|
||||
|
||||
const linuxLinks = reactive({
|
||||
appImage: null as string | null,
|
||||
deb: null as string | null,
|
||||
rpm: null as string | null,
|
||||
thirdParty: "https://support.modrinth.com/en/articles/9298760",
|
||||
});
|
||||
|
||||
const macLinks = reactive({
|
||||
universal: null as string | null,
|
||||
});
|
||||
|
||||
const newProjects = homePageProjects.slice(0, 40);
|
||||
const val = Math.ceil(newProjects.length / 6);
|
||||
const rows = ref([
|
||||
const rows = [
|
||||
newProjects.slice(0, val),
|
||||
newProjects.slice(val, val * 2),
|
||||
newProjects.slice(val * 2, val * 3),
|
||||
newProjects.slice(val * 3, val * 4),
|
||||
newProjects.slice(val * 4, val * 5),
|
||||
]);
|
||||
];
|
||||
|
||||
const [{ data: launcherUpdates }] = await Promise.all([
|
||||
await useAsyncData("launcherUpdates", () =>
|
||||
$fetch("https://launcher-files.modrinth.com/updates.json"),
|
||||
),
|
||||
]);
|
||||
const { data: launcherUpdates } = await useFetch<LauncherUpdates>(
|
||||
"https://launcher-files.modrinth.com/updates.json?new",
|
||||
{
|
||||
server: false,
|
||||
getCachedData(key, nuxtApp) {
|
||||
const cached = (nuxtApp.ssrContext?.cache as any)?.[key] || nuxtApp.payload.data[key];
|
||||
if (!cached) return;
|
||||
|
||||
macLinks.universal = launcherUpdates.value.platforms["darwin-aarch64"].install_urls[0];
|
||||
windowsLink.value = launcherUpdates.value.platforms["windows-x86_64"].install_urls[0];
|
||||
linuxLinks.appImage = launcherUpdates.value.platforms["linux-x86_64"].install_urls[1];
|
||||
linuxLinks.deb = launcherUpdates.value.platforms["linux-x86_64"].install_urls[0];
|
||||
linuxLinks.rpm = launcherUpdates.value.platforms["linux-x86_64"].install_urls[2];
|
||||
const now = Date.now();
|
||||
const cacheTime = cached._cacheTime || 0;
|
||||
const maxAge = 5 * 60 * 1000;
|
||||
|
||||
onMounted(() => {
|
||||
os.value = navigator?.platform.toString();
|
||||
os.value = os.value?.includes("Mac")
|
||||
? "Mac"
|
||||
: os.value?.includes("Win")
|
||||
? "Windows"
|
||||
: os.value?.includes("Linux")
|
||||
? "Linux"
|
||||
: null;
|
||||
if (now - cacheTime > maxAge) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return cached;
|
||||
},
|
||||
transform(data) {
|
||||
return {
|
||||
...data,
|
||||
_cacheTime: Date.now(),
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const platform = computed<string>(() => {
|
||||
if (import.meta.server) {
|
||||
const headers = useRequestHeaders();
|
||||
return headers["user-agent"] || "";
|
||||
} else {
|
||||
return navigator.userAgent || "";
|
||||
}
|
||||
});
|
||||
const os = computed<OSType>(() => {
|
||||
if (platform.value.includes("Mac")) {
|
||||
return "Mac";
|
||||
} else if (platform.value.includes("Win")) {
|
||||
return "Windows";
|
||||
} else if (platform.value.includes("Linux")) {
|
||||
return "Linux";
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
const downloadLauncher = computed(() => {
|
||||
if (os.value === "Windows") {
|
||||
downloadLauncher = () => {
|
||||
downloadWindows.value.click();
|
||||
return () => {
|
||||
downloadWindows.value?.click();
|
||||
};
|
||||
} else if (os.value === "Linux") {
|
||||
downloadLauncher = () => {
|
||||
downloadLinux.value.click();
|
||||
return () => {
|
||||
downloadLinux.value?.click();
|
||||
};
|
||||
} else {
|
||||
downloadLauncher = () => {
|
||||
return () => {
|
||||
scrollToSection();
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const handleDownload = () => {
|
||||
downloadLauncher.value();
|
||||
};
|
||||
|
||||
watch(
|
||||
launcherUpdates,
|
||||
(newData) => {
|
||||
if (newData?.platforms) {
|
||||
macLinks.universal = newData.platforms["darwin-aarch64"]?.install_urls[0] || null;
|
||||
windowsLink.value = newData.platforms["windows-x86_64"]?.install_urls[0] || null;
|
||||
linuxLinks.appImage = newData.platforms["linux-x86_64"]?.install_urls[1] || null;
|
||||
linuxLinks.deb = newData.platforms["linux-x86_64"]?.install_urls[0] || null;
|
||||
linuxLinks.rpm = newData.platforms["linux-x86_64"]?.install_urls[2] || null;
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const scrollToSection = () => {
|
||||
nextTick(() => {
|
||||
window.scrollTo({
|
||||
top: downloadSection.value.offsetTop,
|
||||
behavior: "smooth",
|
||||
});
|
||||
if (downloadSection.value) {
|
||||
window.scrollTo({
|
||||
top: downloadSection.value.offsetTop,
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -119,7 +177,7 @@ useSeoMeta({
|
||||
v-if="os"
|
||||
class="iconified-button brand-button btn btn-large"
|
||||
rel="noopener nofollow"
|
||||
@click="downloadLauncher"
|
||||
@click="handleDownload"
|
||||
>
|
||||
<svg
|
||||
v-if="os === 'Linux'"
|
||||
@@ -485,7 +543,7 @@ useSeoMeta({
|
||||
class="project button-animation gradient-border"
|
||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}`"
|
||||
>
|
||||
<Avatar :src="project.icon_url" :alt="project.title" size="sm" loading="lazy" />
|
||||
<Avatar :src="project.icon_url!" :alt="project.title" size="sm" />
|
||||
<div class="project-info">
|
||||
<span class="title">
|
||||
{{ project.title }}
|
||||
@@ -596,9 +654,7 @@ useSeoMeta({
|
||||
</div>
|
||||
<div class="description">
|
||||
Modrinth’s launcher is fully open source. You can view the source code on our
|
||||
<a href="https://github.com/modrinth/theseus" rel="noopener" :target="$external()"
|
||||
>GitHub</a
|
||||
>!
|
||||
<a href="https://github.com/modrinth/theseus" rel="noopener" target="_blank">GitHub</a>!
|
||||
</div>
|
||||
</div>
|
||||
<div class="point">
|
||||
@@ -788,7 +844,7 @@ useSeoMeta({
|
||||
Windows
|
||||
</div>
|
||||
<div class="description">
|
||||
<a ref="downloadWindows" :href="windowsLink" download="">
|
||||
<a ref="downloadWindows" :href="windowsLink || undefined" download="">
|
||||
<DownloadIcon />
|
||||
<span> Download the beta </span>
|
||||
</a>
|
||||
@@ -812,7 +868,7 @@ useSeoMeta({
|
||||
Mac
|
||||
</div>
|
||||
<div class="description apple">
|
||||
<a :href="macLinks.universal" download="">
|
||||
<a :href="macLinks.universal || undefined" download="">
|
||||
<DownloadIcon />
|
||||
<span> Download the beta </span>
|
||||
</a>
|
||||
@@ -849,19 +905,19 @@ useSeoMeta({
|
||||
Linux
|
||||
</div>
|
||||
<div class="description apple">
|
||||
<a ref="downloadLinux" :href="linuxLinks.appImage" download="">
|
||||
<a ref="downloadLinux" :href="linuxLinks.appImage || undefined" download="">
|
||||
<DownloadIcon />
|
||||
<span> Download the AppImage </span>
|
||||
</a>
|
||||
<a :href="linuxLinks.deb" download="">
|
||||
<a :href="linuxLinks.deb || undefined" download="">
|
||||
<DownloadIcon />
|
||||
<span> Download the DEB </span>
|
||||
</a>
|
||||
<a :href="linuxLinks.rpm" download="">
|
||||
<a :href="linuxLinks.rpm || undefined" download="">
|
||||
<DownloadIcon />
|
||||
<span> Download the RPM </span>
|
||||
</a>
|
||||
<a :href="linuxLinks.thirdParty" download="">
|
||||
<a :href="linuxLinks.thirdParty || undefined" download="">
|
||||
<LinkIcon />
|
||||
<span> Third-party packages </span>
|
||||
</a>
|
||||
@@ -1365,7 +1421,8 @@ useSeoMeta({
|
||||
width: 25rem;
|
||||
height: 25rem;
|
||||
opacity: 0.75;
|
||||
background: radial-gradient(
|
||||
background:
|
||||
radial-gradient(
|
||||
50% 50% at 50% 50%,
|
||||
rgba(5, 206, 69, 0.19) 0%,
|
||||
rgba(15, 19, 49, 0.25) 100%
|
||||
|
||||
@@ -266,12 +266,12 @@ const getRangeOfMethod = (method) => {
|
||||
|
||||
const maxWithdrawAmount = computed(() => {
|
||||
const interval = selectedMethod.value.interval;
|
||||
return interval?.standard ? interval.standard.max : interval?.fixed?.values.slice(-1)[0] ?? 0;
|
||||
return interval?.standard ? interval.standard.max : (interval?.fixed?.values.slice(-1)[0] ?? 0);
|
||||
});
|
||||
|
||||
const minWithdrawAmount = computed(() => {
|
||||
const interval = selectedMethod.value.interval;
|
||||
return interval?.standard ? interval.standard.min : interval?.fixed?.values?.[0] ?? fees.value;
|
||||
return interval?.standard ? interval.standard.min : (interval?.fixed?.values?.[0] ?? fees.value);
|
||||
});
|
||||
|
||||
const withdrawAccount = computed(() => {
|
||||
|
||||
@@ -212,6 +212,10 @@ if (projects.value) {
|
||||
|
||||
async function goToProjects() {
|
||||
const project = projectsFiltered.value[0];
|
||||
const remainingProjectIds = projectsFiltered.value.slice(1).map((p) => p.id);
|
||||
|
||||
localStorage.setItem("moderation-future-projects", JSON.stringify(remainingProjectIds));
|
||||
|
||||
await router.push({
|
||||
name: "type-id",
|
||||
params: {
|
||||
@@ -220,7 +224,6 @@ async function goToProjects() {
|
||||
},
|
||||
state: {
|
||||
showChecklist: true,
|
||||
projects: projectsFiltered.value.slice(1).map((x) => (x.slug ? x.slug : x.id)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonStyled } from "@modrinth/ui";
|
||||
import { Avatar, ButtonStyled } from "@modrinth/ui";
|
||||
import { RssIcon, GitGraphIcon } from "@modrinth/assets";
|
||||
import dayjs from "dayjs";
|
||||
import { articles as rawArticles } from "@modrinth/blog";
|
||||
import { computed } from "vue";
|
||||
import type { User } from "@modrinth/utils";
|
||||
import ShareArticleButtons from "~/components/ui/ShareArticleButtons.vue";
|
||||
import NewsletterButton from "~/components/ui/NewsletterButton.vue";
|
||||
|
||||
@@ -20,7 +21,21 @@ if (!rawArticle) {
|
||||
});
|
||||
}
|
||||
|
||||
const html = await rawArticle.html();
|
||||
const authorsUrl = `users?ids=${JSON.stringify(rawArticle.authors)}`;
|
||||
|
||||
const [authors, html] = await Promise.all([
|
||||
rawArticle.authors
|
||||
? useAsyncData(authorsUrl, () => useBaseFetch(authorsUrl)).then((data) => {
|
||||
const users = data.data as Ref<User[]>;
|
||||
users.value.sort((a, b) => {
|
||||
return rawArticle.authors.indexOf(a.id) - rawArticle.authors.indexOf(b.id);
|
||||
});
|
||||
|
||||
return users;
|
||||
})
|
||||
: Promise.resolve(),
|
||||
rawArticle.html(),
|
||||
]);
|
||||
|
||||
const article = computed(() => ({
|
||||
...rawArticle,
|
||||
@@ -34,6 +49,8 @@ const article = computed(() => ({
|
||||
html,
|
||||
}));
|
||||
|
||||
const authorCount = computed(() => authors?.value?.length ?? 0);
|
||||
|
||||
const articleTitle = computed(() => article.value.title);
|
||||
const articleUrl = computed(() => `https://modrinth.com/news/article/${route.params.slug}`);
|
||||
|
||||
@@ -83,9 +100,35 @@ useSeoMeta({
|
||||
<article class="mt-6 flex flex-col gap-4 px-6">
|
||||
<h2 class="m-0 text-2xl font-extrabold leading-tight sm:text-4xl">{{ article.title }}</h2>
|
||||
<p class="m-0 text-base leading-tight sm:text-lg">{{ article.summary }}</p>
|
||||
<div class="mt-auto text-sm text-secondary sm:text-base">
|
||||
Posted on {{ dayjsDate.format("MMMM D, YYYY") }}
|
||||
<div class="mt-auto flex flex-wrap items-center gap-1 text-sm text-secondary sm:text-base">
|
||||
<template v-for="(author, index) in authors" :key="`author-${author.id}`">
|
||||
<span v-if="authorCount - 1 === index && authorCount > 1">and</span>
|
||||
<span class="flex items-center">
|
||||
<nuxt-link
|
||||
:to="`/user/${author.id}`"
|
||||
class="inline-flex items-center gap-1 font-semibold hover:underline hover:brightness-[--hover-brightness]"
|
||||
>
|
||||
<Avatar :src="author.avatar_url" circle size="24px" />
|
||||
{{ author.username }}
|
||||
</nuxt-link>
|
||||
<span v-if="(authors?.length ?? 0) > 2 && index !== authorCount - 1">,</span>
|
||||
</span>
|
||||
</template>
|
||||
<template v-if="!authors || authorCount === 0">
|
||||
<nuxt-link
|
||||
to="/organization/modrinth"
|
||||
class="inline-flex items-center gap-1 font-semibold hover:underline hover:brightness-[--hover-brightness]"
|
||||
>
|
||||
<Avatar src="https://cdn-raw.modrinth.com/modrinth-icon-96.webp" size="24px" />
|
||||
Modrinth Team
|
||||
</nuxt-link>
|
||||
</template>
|
||||
<span class="hidden md:block">•</span>
|
||||
<span class="hidden md:block"> {{ dayjsDate.format("MMMM D, YYYY") }}</span>
|
||||
</div>
|
||||
<span class="text-sm text-secondary sm:text-base md:hidden">
|
||||
Posted on {{ dayjsDate.format("MMMM D, YYYY") }}</span
|
||||
>
|
||||
<ShareArticleButtons :title="article.title" :url="articleUrl" />
|
||||
<img
|
||||
:src="article.thumbnail"
|
||||
|
||||
@@ -149,7 +149,8 @@ onMounted(() => {
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.main-hero {
|
||||
background: linear-gradient(360deg, rgba(199, 138, 255, 0.2) 10.92%, var(--color-bg) 100%),
|
||||
background:
|
||||
linear-gradient(360deg, rgba(199, 138, 255, 0.2) 10.92%, var(--color-bg) 100%),
|
||||
var(--color-accent-contrast);
|
||||
margin-top: -5rem;
|
||||
padding: 11.25rem 1rem 8rem;
|
||||
|
||||
@@ -427,11 +427,8 @@
|
||||
Do Modrinth Servers have DDoS protection?
|
||||
</summary>
|
||||
<p class="m-0 ml-6 leading-[160%]">
|
||||
Yes. All Modrinth Servers come with DDoS protection powered by
|
||||
<a href="https://us.ovhcloud.com/security/anti-ddos/" target="_blank"
|
||||
>OVHcloud® Anti-DDoS infrastructure</a
|
||||
>
|
||||
which has over 17Tbps capacity. Your server is safe on Modrinth.
|
||||
Yes. All Modrinth Servers come with DDoS protection, with up to 17Tbps capacity in
|
||||
some locations.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
@@ -443,8 +440,9 @@
|
||||
Where are Modrinth Servers located? Can I choose a region?
|
||||
</summary>
|
||||
<p class="m-0 ml-6 leading-[160%]">
|
||||
We have servers in both North America in Vint Hill, Virginia, and Europe in Limburg,
|
||||
Germany. More regions to come in the future!
|
||||
We have servers available in North America and Europe at the moment that you can
|
||||
choose upon purchase. More regions to come in the future! If you'd like to switch
|
||||
your region, please contact support.
|
||||
</p>
|
||||
</details>
|
||||
|
||||
@@ -719,31 +717,32 @@ async function fetchCapacityStatuses(customProduct = null) {
|
||||
product.metadata.ram < min.metadata.ram ? product : min,
|
||||
),
|
||||
];
|
||||
const capacityChecks = productsToCheck.map((product) =>
|
||||
useServersFetch("stock", {
|
||||
method: "POST",
|
||||
body: {
|
||||
cpu: product.metadata.cpu,
|
||||
memory_mb: product.metadata.ram,
|
||||
swap_mb: product.metadata.swap,
|
||||
storage_mb: product.metadata.storage,
|
||||
},
|
||||
bypassAuth: true,
|
||||
}),
|
||||
);
|
||||
|
||||
const results = await Promise.all(capacityChecks);
|
||||
const capacityChecks = [];
|
||||
for (const product of productsToCheck) {
|
||||
capacityChecks.push(
|
||||
useServersFetch("stock", {
|
||||
method: "POST",
|
||||
body: {
|
||||
cpu: product.metadata.cpu,
|
||||
memory_mb: product.metadata.ram,
|
||||
swap_mb: product.metadata.swap,
|
||||
storage_mb: product.metadata.storage,
|
||||
},
|
||||
bypassAuth: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (customProduct?.metadata) {
|
||||
return {
|
||||
custom: results[0],
|
||||
custom: await capacityChecks[0],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
small: results[0],
|
||||
medium: results[1],
|
||||
large: results[2],
|
||||
custom: results[3],
|
||||
small: await capacityChecks[0],
|
||||
medium: await capacityChecks[1],
|
||||
large: await capacityChecks[2],
|
||||
custom: await capacityChecks[3],
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -760,6 +759,11 @@ async function fetchCapacityStatuses(customProduct = null) {
|
||||
const { data: capacityStatuses, refresh: refreshCapacity } = await useAsyncData(
|
||||
"ServerCapacityAll",
|
||||
fetchCapacityStatuses,
|
||||
{
|
||||
getCachedData() {
|
||||
return null; // Dont cache stock data.
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const isSmallAtCapacity = computed(() => capacityStatuses.value?.small?.available === 0);
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="server.moduleErrors?.general?.error.statusCode === 503"
|
||||
v-else-if="server.moduleErrors?.general?.error || !nodeAccessible"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
<ErrorInformationCard
|
||||
@@ -68,22 +68,22 @@
|
||||
<template #description>
|
||||
<div class="text-md space-y-4">
|
||||
<p class="leading-[170%] text-secondary">
|
||||
Your server's node, where your Modrinth Server is physically hosted, is experiencing
|
||||
issues. We are working with our datacenter to resolve the issue as quickly as possible.
|
||||
Your server's node, where your Modrinth Server is physically hosted, is not accessible
|
||||
at the moment. We are working to resolve the issue as quickly as possible.
|
||||
</p>
|
||||
<p class="leading-[170%] text-secondary">
|
||||
Your data is safe and will not be lost, and your server will be back online as soon as
|
||||
the issue is resolved.
|
||||
</p>
|
||||
<p class="leading-[170%] text-secondary">
|
||||
For updates, please join the Modrinth Discord or contact Modrinth Support via the chat
|
||||
If reloading does not work initially, please contact Modrinth Support via the chat
|
||||
bubble in the bottom right corner and we'll be happy to help.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</ErrorInformationCard>
|
||||
</div>
|
||||
<div
|
||||
<!-- <div
|
||||
v-else-if="server.moduleErrors?.general?.error"
|
||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||
>
|
||||
@@ -96,19 +96,14 @@
|
||||
>
|
||||
<template #description>
|
||||
<div class="space-y-4">
|
||||
<div class="text-center text-secondary">
|
||||
{{
|
||||
formattedTime == "00" ? "Reconnecting..." : `Retrying in ${formattedTime} seconds...`
|
||||
}}
|
||||
</div>
|
||||
<p class="text-lg text-secondary">
|
||||
Something went wrong, and we couldn't connect to your server. This is likely due to a
|
||||
temporary network issue. You'll be reconnected automatically.
|
||||
temporary network issue.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</ErrorInformationCard>
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- SERVER START -->
|
||||
<div
|
||||
v-else-if="serverData"
|
||||
@@ -355,7 +350,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, watch, type Reactive } from "vue";
|
||||
import { ref, computed, onMounted, onUnmounted, type Reactive } from "vue";
|
||||
import {
|
||||
SettingsIcon,
|
||||
CopyIcon,
|
||||
@@ -371,15 +366,15 @@ import DOMPurify from "dompurify";
|
||||
import { ButtonStyled, ErrorInformationCard, ServerNotice } from "@modrinth/ui";
|
||||
import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
|
||||
import type { MessageDescriptor } from "@vintl/vintl";
|
||||
import type {
|
||||
ServerState,
|
||||
Stats,
|
||||
WSEvent,
|
||||
WSInstallationResultEvent,
|
||||
Backup,
|
||||
PowerAction,
|
||||
import {
|
||||
type ServerState,
|
||||
type Stats,
|
||||
type WSEvent,
|
||||
type WSInstallationResultEvent,
|
||||
type Backup,
|
||||
type PowerAction,
|
||||
} from "@modrinth/utils";
|
||||
import { reloadNuxtApp, navigateTo } from "#app";
|
||||
import { reloadNuxtApp } from "#app";
|
||||
import { useModrinthServersConsole } from "~/store/console.ts";
|
||||
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||
import { ModrinthServer, useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
|
||||
@@ -392,7 +387,6 @@ const socket = ref<WebSocket | null>(null);
|
||||
const isReconnecting = ref(false);
|
||||
const isLoading = ref(true);
|
||||
const reconnectInterval = ref<ReturnType<typeof setInterval> | null>(null);
|
||||
const isFirstMount = ref(true);
|
||||
const isMounted = ref(true);
|
||||
const flags = useFeatureFlags();
|
||||
|
||||
@@ -422,18 +416,6 @@ const loadModulesPromise = Promise.resolve().then(() => {
|
||||
|
||||
provide("modulesLoaded", loadModulesPromise);
|
||||
|
||||
watch(
|
||||
() => [server.moduleErrors?.general, server.moduleErrors?.ws],
|
||||
([generalError, wsError]) => {
|
||||
if (server.general?.status === "suspended") return;
|
||||
|
||||
const error = generalError?.error || wsError?.error;
|
||||
if (error && error.statusCode !== 403) {
|
||||
startPolling();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const errorTitle = ref("Error");
|
||||
const errorMessage = ref("An unexpected error occurred.");
|
||||
const errorLog = ref("");
|
||||
@@ -697,7 +679,6 @@ const startUptimeUpdates = () => {
|
||||
const stopUptimeUpdates = () => {
|
||||
if (uptimeIntervalId) {
|
||||
clearInterval(uptimeIntervalId);
|
||||
pollingIntervalId = null;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -836,8 +817,6 @@ const handleInstallationResult = async (data: WSInstallationResultEvent) => {
|
||||
case "ok": {
|
||||
if (!serverData.value) break;
|
||||
|
||||
stopPolling();
|
||||
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
@@ -992,14 +971,6 @@ const notifyError = (title: string, text: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
let pollingIntervalId: ReturnType<typeof setInterval> | null = null;
|
||||
const countdown = ref(15);
|
||||
|
||||
const formattedTime = computed(() => {
|
||||
const seconds = countdown.value % 60;
|
||||
return `${seconds.toString().padStart(2, "0")}`;
|
||||
});
|
||||
|
||||
export type BackupInProgressReason = {
|
||||
type: string;
|
||||
tooltip: MessageDescriptor;
|
||||
@@ -1035,54 +1006,6 @@ const backupInProgress = computed(() => {
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollingIntervalId) {
|
||||
clearTimeout(pollingIntervalId);
|
||||
pollingIntervalId = null;
|
||||
}
|
||||
};
|
||||
|
||||
const startPolling = () => {
|
||||
stopPolling();
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = 10;
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
await server.refresh(["general", "ws"]);
|
||||
|
||||
if (!server.moduleErrors?.general?.error) {
|
||||
stopPolling();
|
||||
connectWebSocket();
|
||||
return;
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
if (retryCount >= maxRetries) {
|
||||
console.error("Max retries reached, stopping polling");
|
||||
stopPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
// Exponential backoff: 3s, 6s, 12s, 24s, etc.
|
||||
const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000);
|
||||
|
||||
pollingIntervalId = setTimeout(poll, delay);
|
||||
} catch (error) {
|
||||
console.error("Polling failed:", error);
|
||||
retryCount++;
|
||||
|
||||
if (retryCount < maxRetries) {
|
||||
const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000);
|
||||
pollingIntervalId = setTimeout(poll, delay);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
};
|
||||
|
||||
const nodeUnavailableDetails = computed(() => [
|
||||
{
|
||||
label: "Server ID",
|
||||
@@ -1091,9 +1014,16 @@ const nodeUnavailableDetails = computed(() => [
|
||||
},
|
||||
{
|
||||
label: "Node",
|
||||
value: server.general?.datacenter ?? "Unknown! Please contact support!",
|
||||
value: server.general?.datacenter ?? "Unknown",
|
||||
type: "inline" as const,
|
||||
},
|
||||
{
|
||||
label: "Error message",
|
||||
value: nodeAccessible.value
|
||||
? (server.moduleErrors?.general?.error.message ?? "Unknown")
|
||||
: "Unable to reach node. Ping test failed.",
|
||||
type: "block" as const,
|
||||
},
|
||||
]);
|
||||
|
||||
const suspendedDescription = computed(() => {
|
||||
@@ -1160,16 +1090,10 @@ const generalErrorAction = computed(() => ({
|
||||
}));
|
||||
|
||||
const nodeUnavailableAction = computed(() => ({
|
||||
label: "Join Modrinth Discord",
|
||||
onClick: () => navigateTo("https://discord.modrinth.com", { external: true }),
|
||||
color: "standard" as const,
|
||||
}));
|
||||
|
||||
const connectionLostAction = computed(() => ({
|
||||
label: "Reload",
|
||||
onClick: () => reloadNuxtApp(),
|
||||
color: "brand" as const,
|
||||
disabled: formattedTime.value !== "00",
|
||||
disabled: false,
|
||||
}));
|
||||
|
||||
const copyServerDebugInfo = () => {
|
||||
@@ -1193,7 +1117,6 @@ const cleanup = () => {
|
||||
|
||||
shutdown();
|
||||
|
||||
stopPolling();
|
||||
stopUptimeUpdates();
|
||||
if (reconnectInterval.value) {
|
||||
clearInterval(reconnectInterval.value);
|
||||
@@ -1236,16 +1159,31 @@ async function dismissNotice(noticeId: number) {
|
||||
await server.refresh(["general"]);
|
||||
}
|
||||
|
||||
const nodeAccessible = ref(true);
|
||||
|
||||
onMounted(() => {
|
||||
isMounted.value = true;
|
||||
if (server.general?.status === "suspended") {
|
||||
isLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
server
|
||||
.testNodeReachability()
|
||||
.then((result) => {
|
||||
nodeAccessible.value = result;
|
||||
if (!nodeAccessible.value) {
|
||||
isLoading.value = false;
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error testing node reachability:", err);
|
||||
nodeAccessible.value = false;
|
||||
isLoading.value = false;
|
||||
});
|
||||
|
||||
if (server.moduleErrors.general?.error) {
|
||||
if (!server.moduleErrors.general?.error?.message?.includes("Forbidden")) {
|
||||
startPolling();
|
||||
}
|
||||
isLoading.value = false;
|
||||
} else {
|
||||
connectWebSocket();
|
||||
}
|
||||
@@ -1297,21 +1235,6 @@ onUnmounted(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => serverData.value?.status,
|
||||
(newStatus, oldStatus) => {
|
||||
if (isFirstMount.value) {
|
||||
isFirstMount.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (newStatus === "installing" && oldStatus !== "installing") {
|
||||
countdown.value = 15;
|
||||
startPolling();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
definePageMeta({
|
||||
middleware: "auth",
|
||||
});
|
||||
@@ -1354,7 +1277,8 @@ useHead({
|
||||
background-repeat: no-repeat;
|
||||
filter: blur(1rem);
|
||||
content: "";
|
||||
background-image: linear-gradient(
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
rgba(from var(--color-raised-bg) r g b / 0.2),
|
||||
rgb(from var(--color-raised-bg) r g b / 0.8)
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
<span :class="{ invisible: 'current_file' in op && !op.current_file }">
|
||||
{{
|
||||
"current_file" in op
|
||||
? op.current_file?.split("/")?.pop() ?? "unknown"
|
||||
? (op.current_file?.split("/")?.pop() ?? "unknown")
|
||||
: "unknown"
|
||||
}}
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Contact: mailto:jai@modrinth.com
|
||||
Expires: 2025-12-31T00:00:00.000Z
|
||||
Preferred-Languages: en
|
||||
Canonical: https://modrinth.com/.well-known/security.txt
|
||||
Policy: https://modrinth.com/legal/security
|
||||
Hiring: https://careers.modrinth.com/
|
||||
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 31 KiB |
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"articles": [
|
||||
{
|
||||
"title": "Skins — Now in Modrinth App!",
|
||||
"summary": "Customize your look, save your favorite skins, and swap them out in a flash, all within Modrinth App.",
|
||||
"thumbnail": "https://modrinth.com/news/article/skins-now-in-modrinth-app/thumbnail.webp",
|
||||
"date": "2025-07-06T23:45:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/skins-now-in-modrinth-app"
|
||||
},
|
||||
{
|
||||
"title": "Creator Updates, July 2025",
|
||||
"summary": "Addressing recent growth and growing pains that have been affecting creators.",
|
||||
@@ -9,7 +16,7 @@
|
||||
},
|
||||
{
|
||||
"title": "A Pride Month Success: Over $8,400 Raised for The Trevor Project!",
|
||||
"summary": "A reflection on our Pride Month fundraiser campaign, which raised thousands for LGBTQ+ youth.",
|
||||
"summary": "Reflecting on our Pride Month fundraiser campaign for LGBTQ+ youth.",
|
||||
"thumbnail": "https://modrinth.com/news/article/pride-campaign-2025/thumbnail.webp",
|
||||
"date": "2025-07-01T18:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/pride-campaign-2025"
|
||||
@@ -91,13 +98,6 @@
|
||||
"date": "2023-02-01T20:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/accelerating-development"
|
||||
},
|
||||
{
|
||||
"title": "Two years of Modrinth: a retrospective",
|
||||
"summary": "The history of Modrinth as we know it from December 2020 to December 2022.",
|
||||
"thumbnail": "https://modrinth.com/news/default.webp",
|
||||
"date": "2023-01-07T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/two-years-of-modrinth-history"
|
||||
},
|
||||
{
|
||||
"title": "Modrinth's Anniversary Update",
|
||||
"summary": "Marking two years of Modrinth and discussing our New Year's Resolutions for 2023.",
|
||||
@@ -105,16 +105,23 @@
|
||||
"date": "2023-01-07T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/two-years-of-modrinth"
|
||||
},
|
||||
{
|
||||
"title": "Two years of Modrinth: a retrospective",
|
||||
"summary": "The history of Modrinth as we know it from December 2020 to December 2022.",
|
||||
"thumbnail": "https://modrinth.com/news/default.webp",
|
||||
"date": "2023-01-07T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/two-years-of-modrinth-history"
|
||||
},
|
||||
{
|
||||
"title": "Creators can now make money on Modrinth!",
|
||||
"summary": "Yes, you read the title correctly: Modrinth's creator monetization program, also known as payouts, is now in an open beta phase. Read on for more information!",
|
||||
"summary": "Introducing the Creator Monetization Program allowing creators to earn revenue from their projects.",
|
||||
"thumbnail": "https://modrinth.com/news/article/creator-monetization/thumbnail.webp",
|
||||
"date": "2022-11-12T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/creator-monetization"
|
||||
},
|
||||
{
|
||||
"title": "Modrinth's Carbon Ads experiment",
|
||||
"summary": "As a step towards implementing author payouts, we're experimenting with a couple different ad providers to see which one works the best for us.",
|
||||
"summary": "Experimenting with a different ad providers to find one which one works for us.",
|
||||
"thumbnail": "https://modrinth.com/news/article/carbon-ads/thumbnail.webp",
|
||||
"date": "2022-09-08T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/carbon-ads"
|
||||
@@ -142,14 +149,14 @@
|
||||
},
|
||||
{
|
||||
"title": "This week in Modrinth development: Filters and Fixes",
|
||||
"summary": "After a great first week since Modrinth launched out of beta, we have continued to improve the user interface based on feedback.",
|
||||
"summary": "Continuing to improve the user interface after a great first week since Modrinth launched out of beta.",
|
||||
"thumbnail": "https://modrinth.com/news/article/knossos-v2.1.0/thumbnail.webp",
|
||||
"date": "2022-03-09T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/knossos-v2.1.0"
|
||||
},
|
||||
{
|
||||
"title": "Now showing on Modrinth: A new look!",
|
||||
"summary": "After months of relatively quiet development, Modrinth has released many new features and improvements, including a redesign. Read on to learn more!",
|
||||
"summary": "Releasing many new features and improvements, including a redesign!",
|
||||
"thumbnail": "https://modrinth.com/news/article/redesign/thumbnail.webp",
|
||||
"date": "2022-02-27T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/redesign"
|
||||
|
||||
@@ -4,20 +4,28 @@
|
||||
<description><![CDATA[Keep up-to-date on the latest news from Modrinth.]]></description>
|
||||
<link>https://modrinth.com/news/</link>
|
||||
<generator>@modrinth/blog</generator>
|
||||
<lastBuildDate>Wed, 02 Jul 2025 02:42:05 GMT</lastBuildDate>
|
||||
<lastBuildDate>Tue, 15 Jul 2025 12:02:34 GMT</lastBuildDate>
|
||||
<atom:link href="https://modrinth.com/news/feed/rss.xml" rel="self" type="application/rss+xml"/>
|
||||
<language><![CDATA[en]]></language>
|
||||
<item>
|
||||
<title><![CDATA[Skins — Now in Modrinth App!]]></title>
|
||||
<description><![CDATA[Customize your look, save your favorite skins, and swap them out in a flash, all within Modrinth App.]]></description>
|
||||
<link>https://modrinth.com/news/article/skins-now-in-modrinth-app/</link>
|
||||
<guid isPermaLink="false">https://modrinth.com/news/article/skins-now-in-modrinth-app/</guid>
|
||||
<pubDate>Sun, 06 Jul 2025 23:45:00 GMT</pubDate>
|
||||
<content:encoded><![CDATA[<p>We're thrilled to roll out Modrinth App <strong>v0.10</strong> with a beta release of one of our most highly requested features, the <strong>Skins page</strong>. The Skins page allows you to manage all of your Minecraft skins directly within Modrinth App. You can see all your saved custom skins and the default Minecraft skins in one convenient place.</p><p><img src="./skins-page.webp" alt="The new skins page, featuring a cute animated player model, your custom skins &amp; default skins."></p><p>Adding a new skin is simple, even Herobrine could do it! When you add or edit a skin, you can <strong>upload</strong> your custom texture file directly from your computer, <strong>choose</strong> between the wide or slim arm style to match your preferred character model, and even <strong>assign</strong> a specific cape to that look for the perfect finishing touch.</p><p>The interface makes it easy to preview your changes in real-time with the animated player model, so you can see exactly how your skin will look in-game before saving it.</p><p><img src="./edit-skin.webp" alt="The edit skin modal that shows when you go to add or edit a skin."></p><h2>Fixes and More!</h2><p>Alongside this major new feature, <strong>v0.10</strong> includes a host of improvements and bug fixes to make your experience smoother. We've updated the news feed to use our new system, fixed issues with project descriptions, and tidied up how data is handled. For a full breakdown of all the changes, you can <a href="https://modrinth.com/news/changelog?filter=app">check out the complete changelog here.</a></p><p>As the skins feature is in <em>beta</em>, we're eager to hear your feedback! <strong>Jump in, give it a try</strong>, and let us know what you think. You can share your thoughts on our <a href="https://discord.modrinth.com/" rel="noopener nofollow ugc">Discord server</a> or <a href="https://support.modrinth.com" rel="noopener nofollow ugc">start a support chat</a> if you're running into issues.</p><p>Thank you! We can't wait to see your skins in action. Happy customizing!</p>]]></content:encoded>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Creator Updates, July 2025]]></title>
|
||||
<description><![CDATA[Addressing recent growth and growing pains that have been affecting creators.]]></description>
|
||||
<link>https://modrinth.com/news/article/creator-updates-july-2025/</link>
|
||||
<guid isPermaLink="false">https://modrinth.com/news/article/creator-updates-july-2025/</guid>
|
||||
<pubDate>Wed, 02 Jul 2025 03:00:00 GMT</pubDate>
|
||||
<pubDate>Wed, 02 Jul 2025 04:20:00 GMT</pubDate>
|
||||
<content:encoded><![CDATA[<p>Hey all,</p><p>The last few months have been quite hectic for Modrinth. We've experienced all-time highs in both traffic and new creators and have outgrown a lot of our existing systems, which has led to a lot of issues plaguing creators, especially.</p><p>The team has been super hard at work at this, and I'm really glad to announce that we've fixed most of these issues long term.</p><ol><li><p><strong>Upload issues (inputs not showing up, instability, etc)</strong></p><p>We've tracked these issues down to conflicting code between our ad provider and Modrinth's. For now, we've <strong>disabled ads for all logged in users across the site</strong> while we work on resolving these long term. Both web users and logged-in web users make a very small percentage of our ad revenue (7% for web and 0.05% for logged-in web users) so creators should see a very minimal revenue drop from this, and have a much better experience navigating and uploading to the site.</p></li><li><p><strong>Moderation and report response times</strong></p><p>Creators have had to wait, in some cases, weeks to get their projects reviewed. This is unacceptable on our part and we are actively overhauling our moderation tooling to improve the moderation experience (and lowering time spent per project). We've also hired 3 additional moderators/support staff (<strong>bringing our total to 7 and the total team to 17 people!</strong>). We're hoping to see a significant reduction in queue times over the coming weeks.</p></li><li><p><strong>Ad revenue instability</strong></p><p>While ad revenue is generally out of our control and tends to fluctuate a lot, on June 4th we noticed a sharp decrease in creator revenue (~35% less than normal levels). While our ad provider initially thought this was a display issue, after further inquiry there were 2 causes: 1) Google AdExchange falsely flagging our traffic as invalid 2) Amazon banning many gaming publishers from their network <a href="https://www.adweek.com/media/exclusive-ads-from-verizon-shell-and-others-ran-next-to-explicit-videos-on-top-android-app/" rel="noopener nofollow ugc">due to panic in the gaming ads space</a>. While the Amazon ban is now resolved, we no longer are running Google AdExchange in the desktop app due to invalid traffic issues. This will lead to a permanent revenue decrease (AdX contributed to ~20% of our ad revenue). We also updated our prebid version (the underlying tech used to run ad auctions) which has shown a measurable increase, bringing revenue back to &quot;normal&quot; levels. Overall, we are closely monitoring and will keep you all posted. However, despite all the issues, due to some end-of-quarter campaigns, <strong>revenue in June was an all time high, at $227k ($170k paid to creators)</strong>!</p></li><li><p><strong>Payout outages</strong></p><p>Creators should be able to withdraw their revenue at all times, but due to slow PayPal clearing times and poor planning by us, we've had multiple week long outages in withdrawals. While we do store funds 1:1, these &quot;outages&quot; happen because we primarily store creator funds in an FDIC-insured bank account, as we wouldn't want a PayPal/Tremendous account suspension to cause creators to lose funds. We've now set up internal reporting which should never cause this to happen again (or, if it does, drastically reduce the time payout outages happen)</p></li><li><p><strong>Platform Revenue Route</strong></p><p>Due to some unannounced breaking changes in Aditude's API, the platform revenue API was broken. It is now <a href="https://api.modrinth.com/v3/payout/platform_revenue" rel="noopener nofollow ugc">working</a>. You can also use <code>start</code> and <code>end</code> fields to filter any date range!</p></li><li><p><strong>API and Uptime</strong></p><p>We've migrated our infrastructure for the website, app, and servers to OVH over our existing non-redundant AWS system. We've hit 99.96% uptime on our API and 99.98% on Modrinth Servers!</p></li></ol><p>Thank you all for your patience! If you are having any more issues or have any questions about all of this, feel free to DM @geometrically on Discord or <a href="https://support.modrinth.com" rel="noopener nofollow ugc">start a support chat</a> and we will be happy to help!</p>]]></content:encoded>
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[A Pride Month Success: Over $8,400 Raised for The Trevor Project!]]></title>
|
||||
<description><![CDATA[A reflection on our Pride Month fundraiser campaign, which raised thousands for LGBTQ+ youth.]]></description>
|
||||
<description><![CDATA[Reflecting on our Pride Month fundraiser campaign for LGBTQ+ youth.]]></description>
|
||||
<link>https://modrinth.com/news/article/pride-campaign-2025/</link>
|
||||
<guid isPermaLink="false">https://modrinth.com/news/article/pride-campaign-2025/</guid>
|
||||
<pubDate>Tue, 01 Jul 2025 18:00:00 GMT</pubDate>
|
||||
@@ -111,14 +119,6 @@
|
||||
<pubDate>Wed, 01 Feb 2023 20:00:00 GMT</pubDate>
|
||||
<content:encoded><![CDATA[<p><strong>Update: On <a href="/news/article/capital-return" rel="noopener nofollow ugc">April 4, 2024</a> we announced that we had returned the remaining $800k in investor capital back to our investors to take a different path. <a href="/news/article/capital-return" rel="noopener nofollow ugc">Read that announcement here</a>. This article remains here for archival purposes.</strong></p><hr><p>There are over 3 billion gamers worldwide, but only a small fraction ever go further and mod the games they play. Modrinth is here to bring modding to every player on the planet—all the while allowing mod creators to make a living off of it.</p><p>Since our founding in 2020 and up until a few months ago, Modrinth has been a purely volunteer project. In the past couple months, the Modrinth team has been more productive than ever. We've released the <a href="../two-years-of-modrinth" rel="noopener nofollow ugc">Anniversary Update</a>, we're actively working on the launcher once more, and we're laying out plans for how to multiply Modrinth creator payouts.</p><p>The vision we have for the future of Modrinth is great, and right alongside that is the need for an amazing team to build out this vision. That's why we <a href="https://x.com/modrinth/status/1615416957905342472" rel="noopener nofollow ugc">recently announced</a> that <a href="https://careers.modrinth.com" rel="noopener nofollow ugc">we're hiring</a>—we've already come so far on just volunteer work, but for Modrinth to be sustainable and for its growth to be sustainable, we need to pick up the pace.</p><p>That's why we're excited to announce that we've raised a pre-seed round of funding led by <a href="https://www.makersfund.com/" rel="noopener nofollow ugc">Makers Fund</a>, with investors including <a href="https://x.com/ryanmjohnson" rel="noopener nofollow ugc">Ryan Johnson</a>, <a href="https://x.com/sthenc" rel="noopener nofollow ugc">Stephen Cole</a>, <a href="https://x.com/PimDeWitte" rel="noopener nofollow ugc">Pim de Witte</a>, <a href="https://www.linkedin.com/in/leechris1/" rel="noopener nofollow ugc">Chris Lee</a>, and <a href="https://x.com/andreas" rel="noopener nofollow ugc">Andreas Thorstensson</a> to accelerate development and expand to new horizons for Modrinth.</p><h2>What's next?</h2><p>We're thrilled to keep on building and iterating on Modrinth over the next few years. Here's a look into what we have in store over the next few months for Modrinth:</p><ul><li>A feature-packed launcher</li><li>Creator organizations (like GitHub), wikis, graphs (with playtime, views, downloads, etc)</li><li>More creator payouts, through the growth of Adrinth</li><li>Discovery/recommendation of mods (especially up-and-coming content)</li><li>Comments (with built-in moderation and spam protection)</li><li>[Redacted]</li></ul><p>Support for new games!</p><p>We are excited that we are able to build a product that will manage to grow us to sustainability and create the best modding experience for creators and users. Being able to pay ourselves and bring on new people is a big step in making that happen. There is still a lot to do, so let's get to it!</p><h2>Q&amp;A:</h2><p>We know there might be some concerns so we included a short Q&amp;A section below for some common ones. Feel free to ask in our <a href="https://discord.modrinth.com" rel="noopener nofollow ugc">Discord</a> if you have any more questions!</p><h3>Why does Modrinth need funding?</h3><p>Our main expense is and will continue to be salaries. The labor cost has always been the main bottleneck for Modrinth. Having paid employees will allow us to develop Modrinth faster, bringing Modrinth to a point of sustainability and growing the platform. For example, we're planning to release our launcher this year, and eventually we're hoping to expand into more games. Those won't be possible without having paid employees.</p><h3>Is Modrinth still community-first?</h3><p>We started and always will have the goal of creating a community-oriented, open-source modding platform. Simply put, there isn't any reason for us not to be. It's clear that the previous impersonal, corporate approaches to video game modding have not worked, andLine truncated
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Two years of Modrinth: a retrospective]]></title>
|
||||
<description><![CDATA[The history of Modrinth as we know it from December 2020 to December 2022.]]></description>
|
||||
<link>https://modrinth.com/news/article/two-years-of-modrinth-history/</link>
|
||||
<guid isPermaLink="false">https://modrinth.com/news/article/two-years-of-modrinth-history/</guid>
|
||||
<pubDate>Sat, 07 Jan 2023 00:00:00 GMT</pubDate>
|
||||
<content:encoded><![CDATA[<p>Let's rewind a bit and take a look at the past two years of Modrinth's history. We've come so far from our pre-beta HexFabric days to today. A good portion of our pre-beta history can be found in the <a href="../what-is-modrinth" rel="noopener nofollow ugc">What is Modrinth</a> blog post, but Modrinth obviously is not the same platform it was two years ago.</p><h2>December 2020: Modrinth Beta begins</h2><p><img src="../modrinth-beta/new-design.jpg" alt="Modrinth's brand new design, rolling out with the launch of Beta"></p><blockquote><p>Modrinth's brand new design, rolling out with the launch of Beta</p></blockquote><p>December was the release of the initial <a href="../modrinth-beta" rel="noopener nofollow ugc">Modrinth Beta</a>, bringing a completely different interface and the ability for mods to be created for the first time. This interface has since been completely discarded, but this is what Modrinth looked like for well over a year. It's hard to believe!</p><p>December also brought the introduction of the <a href="https://github.com/modrinth/minotaur" rel="noopener nofollow ugc">Minotaur</a> Gradle plugin for the first time for upload automation. Minotaur today also looks nothing like it did when it was introduced, but it still accomplishes the same exact thing.</p><h2>January 2021: Improvements to mod uploading</h2><p>An announcement in mid-January brought several essential additions and improvements to Modrinth which we consider commonplace today. Among these were:</p><ul><li>A separate version creation page</li><li>The ability to edit and delete existing versions</li><li>The ability to delete existing mods and users</li></ul><p>January also brought the introduction of Google AdSense onto Modrinth. The eventual results, including our switch after to EthicalAds, solidified Modrinth's stance that ads should be unobtrusive and generally friendly to users.</p><h2>February-March 2021: Follows, reports, notifications, oh my!</h2><p>March brought the first introduction of the abilities to follow and report projects, as well as the automatic featuring of some versions depending on loader and Minecraft version. These systems have largely remained unchanged since their introduction, though the notification system will likely be getting a refresh come 2023.</p><h2>April-December 2021: Season of silence</h2><p>After follows, reports, and all that jazz, Modrinth largely went silent for a good while. This time period had some of the largest growth Modrinth had ever seen, and yet it seemed Modrinth's development had ground to a halt. Modrinth Team members were dropping like flies until there was a point where there was a single person on the team. What happened?</p><p>For various reasons, whether it be lack of free time or a lack of interest in Minecraft in general, people ended up leaving to pursue other things. It's not quite as apocalyptic or barren as these descriptions make it seem, but it's more fun to describe it like this.</p><p>Picking up the remnants from what others had left behind, one man was destined to continue developing for Modrinth. The one who began the whole operation in the first place, Geometrically, stood up and began developing. Thus came the development of project types, gallery images, API v2, and modpacks.</p><h2>January 2022: API v2 introduction</h2><p>Right around the corner came 2022. Perchance this would be the time for the silence to be broken? Indeed, the world would be able to hear about all that was brewing over the past few months.</p><p>Of course, this wasn't all introduced at once—it was a gradual rollout over several months. First was the introduction of v2 of Modrinth's API, allowing many breaking changes to occur, including namely the renaming of <em>mods</em> to <em>projects</em>. Wait, why was this necessary?</p><p>Up until this point, Modrinth only hosted mods. Project types, as we call them, allow projects to be given the designation of something other than <em>mod</em>; for example, <em>modpack</em> or <em>resourcepack</em>, like we have today. This simple field, alongside all of the infrastructure which was needed to support it, was the first step to allow modpacks on Modrinth.</p><h2>February 2022: Redesign</h2><p>Remember the interface introduced in December 2020? Let's scrap it! Actually, it wasn't entirely scrapped, but it got a treatment similar to the <a href="Line truncated
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Modrinth's Anniversary Update]]></title>
|
||||
<description><![CDATA[Marking two years of Modrinth and discussing our New Year's Resolutions for 2023.]]></description>
|
||||
@@ -127,9 +127,17 @@
|
||||
<pubDate>Sat, 07 Jan 2023 00:00:00 GMT</pubDate>
|
||||
<content:encoded><![CDATA[<p>Modrinth initially <a href="../modrinth-beta" rel="noopener nofollow ugc">went into beta</a> on November 30th, 2020. Just over a month ago was November 30th, 2022, marking <strong>two years</strong> since Modrinth was generally available as a platform for everyone to use. Today, we're proud to announce the Anniversary Update, celebrating both two years of Modrinth as well as the coming of the new year, and we'll be discussing our New Year's Resolutions for 2023.</p><p>Before you read this post, though, we recommend taking a look at <a href="../two-years-of-modrinth-history" rel="noopener nofollow ugc">our retrospective on Modrinth's history through 2020—2022</a>. It just wouldn't be right to take a look at the present and the future without also taking a look at our past, seeing how far we've come from our humble beginnings.</p><p>With that out of the way, this post primarily serves to announce a few of the smaller features we've been working on after the release of creator monetization. We've bundled these all together as the <strong>Anniversary Update</strong>.</p><p>Looking just at what's already done is boring, though, so we'll also be looking at what's yet to come. Modrinth's future is even brighter than any of us can imagine, so we'll be focusing on what we're gonna do in order to get to that bright future. If you've ever made <strong>New Year's Resolutions</strong>, we're going to briefly discuss our resolutions for 2023.</p><p>Without further adieu, let's get right into what's new with this new year!</p><h2>Shader packs and data packs</h2><p>The long-awaited arrival of shader packs and data packs is now here on Modrinth!</p><p>Shader packs can be viewed in the <a href="/shaders" rel="noopener nofollow ugc">shaders tab</a>. This includes shaders that support <a href="/mod/iris" rel="noopener nofollow ugc">Iris</a>, <a href="/mod/canvas" rel="noopener nofollow ugc">Canvas</a>, and OptiFine, as well as vanilla core shaders. (Even though they're installed via the resource pack system, we have decided to put Canvas and core shaders in shader packs since most users will not search in resource packs for shader packs, even if that's how they're installed.)</p><p>Data packs can be found in the <a href="/datapacks" rel="noopener nofollow ugc">data packs tab</a>. These are implemented similarly to plugins, in that projects with a mod version can also upload a data pack version (and vice versa). Additionally, data pack authors can choose to have their data packs packaged as a mod using the handy-dandy button on the site.</p><p>Data packs can optionally upload a corresponding resource pack as a separate file. We discourage bundling data files and asset files in the same zip file.</p><h2>New landing page</h2><p>The <a href="/" rel="noopener nofollow ugc">homepage</a> has been completely remade, featuring a scrolling list of random projects from Modrinth. Feel free to use this to discover new projects—just make sure you refresh occasionally, because they loop after a little while until you refresh!</p><p><img src="./landing-page.jpg" alt="A screenshot of the new homepage, with a maze background and projects scrolling across the bottom. Bold across the front is &quot;The place for Minecraft mods&quot;."></p><h2>Project overhaul for creators</h2><p>We're continuing to bring expansions to the creator dashboard introduced with monetization. The new <strong>Projects</strong> tab allows you to view all of your projects in a table and quickly access their information and settings.</p><p><a href="/dashboard/projects" rel="noopener nofollow ugc"><img src="./projects-dashboard.jpg" alt="The new Modrinth project dashboard"></a></p><p>The same page also introduces the ability to bulk-edit the external resource links without having to edit each page individually. For example, if your Discord invite expires, you used to have to edit each of your projects individually to add it back. Now you can just select the projects you want to edit the links for and edit them all at the same time!</p><p><img src="./bulk-edit.jpg" alt="A modal with several input fields for external resource links, listing multiple projects the input changes will be applied to."></p><p>Even better Line truncated
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Two years of Modrinth: a retrospective]]></title>
|
||||
<description><![CDATA[The history of Modrinth as we know it from December 2020 to December 2022.]]></description>
|
||||
<link>https://modrinth.com/news/article/two-years-of-modrinth-history/</link>
|
||||
<guid isPermaLink="false">https://modrinth.com/news/article/two-years-of-modrinth-history/</guid>
|
||||
<pubDate>Sat, 07 Jan 2023 00:00:00 GMT</pubDate>
|
||||
<content:encoded><![CDATA[<p>Let's rewind a bit and take a look at the past two years of Modrinth's history. We've come so far from our pre-beta HexFabric days to today. A good portion of our pre-beta history can be found in the <a href="../what-is-modrinth" rel="noopener nofollow ugc">What is Modrinth</a> blog post, but Modrinth obviously is not the same platform it was two years ago.</p><h2>December 2020: Modrinth Beta begins</h2><p><img src="../modrinth-beta/new-design.jpg" alt="Modrinth's brand new design, rolling out with the launch of Beta"></p><blockquote><p>Modrinth's brand new design, rolling out with the launch of Beta</p></blockquote><p>December was the release of the initial <a href="../modrinth-beta" rel="noopener nofollow ugc">Modrinth Beta</a>, bringing a completely different interface and the ability for mods to be created for the first time. This interface has since been completely discarded, but this is what Modrinth looked like for well over a year. It's hard to believe!</p><p>December also brought the introduction of the <a href="https://github.com/modrinth/minotaur" rel="noopener nofollow ugc">Minotaur</a> Gradle plugin for the first time for upload automation. Minotaur today also looks nothing like it did when it was introduced, but it still accomplishes the same exact thing.</p><h2>January 2021: Improvements to mod uploading</h2><p>An announcement in mid-January brought several essential additions and improvements to Modrinth which we consider commonplace today. Among these were:</p><ul><li>A separate version creation page</li><li>The ability to edit and delete existing versions</li><li>The ability to delete existing mods and users</li></ul><p>January also brought the introduction of Google AdSense onto Modrinth. The eventual results, including our switch after to EthicalAds, solidified Modrinth's stance that ads should be unobtrusive and generally friendly to users.</p><h2>February-March 2021: Follows, reports, notifications, oh my!</h2><p>March brought the first introduction of the abilities to follow and report projects, as well as the automatic featuring of some versions depending on loader and Minecraft version. These systems have largely remained unchanged since their introduction, though the notification system will likely be getting a refresh come 2023.</p><h2>April-December 2021: Season of silence</h2><p>After follows, reports, and all that jazz, Modrinth largely went silent for a good while. This time period had some of the largest growth Modrinth had ever seen, and yet it seemed Modrinth's development had ground to a halt. Modrinth Team members were dropping like flies until there was a point where there was a single person on the team. What happened?</p><p>For various reasons, whether it be lack of free time or a lack of interest in Minecraft in general, people ended up leaving to pursue other things. It's not quite as apocalyptic or barren as these descriptions make it seem, but it's more fun to describe it like this.</p><p>Picking up the remnants from what others had left behind, one man was destined to continue developing for Modrinth. The one who began the whole operation in the first place, Geometrically, stood up and began developing. Thus came the development of project types, gallery images, API v2, and modpacks.</p><h2>January 2022: API v2 introduction</h2><p>Right around the corner came 2022. Perchance this would be the time for the silence to be broken? Indeed, the world would be able to hear about all that was brewing over the past few months.</p><p>Of course, this wasn't all introduced at once—it was a gradual rollout over several months. First was the introduction of v2 of Modrinth's API, allowing many breaking changes to occur, including namely the renaming of <em>mods</em> to <em>projects</em>. Wait, why was this necessary?</p><p>Up until this point, Modrinth only hosted mods. Project types, as we call them, allow projects to be given the designation of something other than <em>mod</em>; for example, <em>modpack</em> or <em>resourcepack</em>, like we have today. This simple field, alongside all of the infrastructure which was needed to support it, was the first step to allow modpacks on Modrinth.</p><h2>February 2022: Redesign</h2><p>Remember the interface introduced in December 2020? Let's scrap it! Actually, it wasn't entirely scrapped, but it got a treatment similar to the <a href="Line truncated
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Creators can now make money on Modrinth!]]></title>
|
||||
<description><![CDATA[Yes, you read the title correctly: Modrinth's creator monetization program, also known as payouts, is now in an open beta phase. Read on for more information!]]></description>
|
||||
<description><![CDATA[Introducing the Creator Monetization Program allowing creators to earn revenue from their projects.]]></description>
|
||||
<link>https://modrinth.com/news/article/creator-monetization/</link>
|
||||
<guid isPermaLink="false">https://modrinth.com/news/article/creator-monetization/</guid>
|
||||
<pubDate>Sat, 12 Nov 2022 00:00:00 GMT</pubDate>
|
||||
@@ -137,7 +145,7 @@
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Modrinth's Carbon Ads experiment]]></title>
|
||||
<description><![CDATA[As a step towards implementing author payouts, we're experimenting with a couple different ad providers to see which one works the best for us.]]></description>
|
||||
<description><![CDATA[Experimenting with a different ad providers to find one which one works for us.]]></description>
|
||||
<link>https://modrinth.com/news/article/carbon-ads/</link>
|
||||
<guid isPermaLink="false">https://modrinth.com/news/article/carbon-ads/</guid>
|
||||
<pubDate>Thu, 08 Sep 2022 00:00:00 GMT</pubDate>
|
||||
@@ -169,7 +177,7 @@
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[This week in Modrinth development: Filters and Fixes]]></title>
|
||||
<description><![CDATA[After a great first week since Modrinth launched out of beta, we have continued to improve the user interface based on feedback.]]></description>
|
||||
<description><![CDATA[Continuing to improve the user interface after a great first week since Modrinth launched out of beta.]]></description>
|
||||
<link>https://modrinth.com/news/article/knossos-v2.1.0/</link>
|
||||
<guid isPermaLink="false">https://modrinth.com/news/article/knossos-v2.1.0/</guid>
|
||||
<pubDate>Wed, 09 Mar 2022 00:00:00 GMT</pubDate>
|
||||
@@ -177,7 +185,7 @@
|
||||
</item>
|
||||
<item>
|
||||
<title><![CDATA[Now showing on Modrinth: A new look!]]></title>
|
||||
<description><![CDATA[After months of relatively quiet development, Modrinth has released many new features and improvements, including a redesign. Read on to learn more!]]></description>
|
||||
<description><![CDATA[Releasing many new features and improvements, including a redesign!]]></description>
|
||||
<link>https://modrinth.com/news/article/redesign/</link>
|
||||
<guid isPermaLink="false">https://modrinth.com/news/article/redesign/</guid>
|
||||
<pubDate>Sun, 27 Feb 2022 00:00:00 GMT</pubDate>
|
||||
|
||||
@@ -36,7 +36,7 @@ paste.workspace = true
|
||||
meilisearch-sdk = { workspace = true, features = ["reqwest"] }
|
||||
rust-s3.workspace = true
|
||||
reqwest = { workspace = true, features = ["http2", "rustls-tls-webpki-roots", "json", "multipart"] }
|
||||
hyper-tls.workspace = true
|
||||
hyper-rustls.workspace = true
|
||||
hyper-util.workspace = true
|
||||
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
FROM rust:1.88.0 AS build
|
||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||
|
||||
WORKDIR /usr/src/labrinth
|
||||
COPY . .
|
||||
COPY apps/labrinth/.sqlx/ .sqlx/
|
||||
RUN cargo build --release --package labrinth
|
||||
|
||||
RUN SQLX_OFFLINE=true cargo build --release --package labrinth
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
@@ -14,12 +11,9 @@ LABEL org.opencontainers.image.description="Modrinth API"
|
||||
LABEL org.opencontainers.image.licenses=AGPL-3.0
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates openssl dumb-init \
|
||||
&& apt-get clean \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates dumb-init curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN update-ca-certificates
|
||||
|
||||
COPY --from=build /usr/src/labrinth/target/release/labrinth /labrinth/labrinth
|
||||
COPY --from=build /usr/src/labrinth/apps/labrinth/migrations/* /labrinth/migrations/
|
||||
COPY --from=build /usr/src/labrinth/apps/labrinth/assets /labrinth/assets
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use hyper_tls::{HttpsConnector, native_tls};
|
||||
use hyper_util::client::legacy::connect::HttpConnector;
|
||||
use hyper_rustls::HttpsConnectorBuilder;
|
||||
use hyper_util::rt::TokioExecutor;
|
||||
|
||||
mod fetch;
|
||||
@@ -15,13 +14,11 @@ pub async fn init_client_with_database(
|
||||
database: &str,
|
||||
) -> clickhouse::error::Result<clickhouse::Client> {
|
||||
let client = {
|
||||
let mut http_connector = HttpConnector::new();
|
||||
http_connector.enforce_http(false); // allow https URLs
|
||||
|
||||
let tls_connector =
|
||||
native_tls::TlsConnector::builder().build().unwrap().into();
|
||||
let https_connector =
|
||||
HttpsConnector::from((http_connector, tls_connector));
|
||||
let https_connector = HttpsConnectorBuilder::new()
|
||||
.with_native_roots()?
|
||||
.https_or_http()
|
||||
.enable_all_versions()
|
||||
.build();
|
||||
let hyper_client =
|
||||
hyper_util::client::legacy::Client::builder(TokioExecutor::new())
|
||||
.build(https_connector);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"app:build": "turbo run build --filter=@modrinth/app",
|
||||
"app:fix": "turbo run fix --filter=@modrinth/app",
|
||||
"app:intl:extract": "pnpm run --filter=@modrinth/app-frontend intl:extract",
|
||||
"blog:fix": "turbo run fix --filter=@modrinth/blog",
|
||||
"pages:build": "NITRO_PRESET=cloudflare-pages pnpm --filter frontend run build",
|
||||
"build": "turbo run build --continue",
|
||||
"lint": "turbo run lint --continue",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "theseus"
|
||||
version = "0.10.1"
|
||||
version = "1.0.0-local" # The actual version is set by the theseus-build workflow on tagging
|
||||
authors = ["Jai A <jaiagr+gpg@pm.me>"]
|
||||
edition.workspace = true
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.modrinth.theseus;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.AccessibleObject;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
@@ -76,14 +77,14 @@ public final class MinecraftLaunch {
|
||||
|
||||
Object thisObject = null;
|
||||
if (!Modifier.isStatic(mainMethod.getModifiers())) {
|
||||
thisObject = mainClass.getDeclaredConstructor().newInstance();
|
||||
thisObject = forceAccessible(mainClass.getDeclaredConstructor()).newInstance();
|
||||
}
|
||||
|
||||
final Object[] parameters = mainMethod.getParameterCount() > 0 ? new Object[] {args} : new Object[] {};
|
||||
|
||||
mainMethod.invoke(thisObject, parameters);
|
||||
} else {
|
||||
findSimpleMainMethod(mainClass).invoke(null, new Object[] {args});
|
||||
forceAccessible(findSimpleMainMethod(mainClass)).invoke(null, new Object[] {args});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,4 +116,15 @@ public final class MinecraftLaunch {
|
||||
private static Method findSimpleMainMethod(Class<?> mainClass) throws NoSuchMethodException {
|
||||
return mainClass.getMethod("main", String[].class);
|
||||
}
|
||||
|
||||
private static <T extends AccessibleObject> T forceAccessible(T object) throws ReflectiveOperationException {
|
||||
try {
|
||||
final Method setAccessible0 = AccessibleObject.class.getDeclaredMethod("setAccessible0", boolean.class);
|
||||
setAccessible0.setAccessible(true);
|
||||
setAccessible0.invoke(object, true);
|
||||
} catch (NoSuchMethodException e) {
|
||||
object.setAccessible(true);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
}
|
||||
@@ -166,10 +166,18 @@ pub async fn test_jre(
|
||||
path: PathBuf,
|
||||
major_version: u32,
|
||||
) -> crate::Result<bool> {
|
||||
let Ok(jre) = jre::check_java_at_filepath(&path).await else {
|
||||
return Ok(false);
|
||||
let jre = match jre::check_java_at_filepath(&path).await {
|
||||
Ok(jre) => jre,
|
||||
Err(e) => {
|
||||
tracing::warn!("Invalid Java at {}: {e}", path.display());
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
let version = extract_java_version(&jre.version)?;
|
||||
tracing::info!(
|
||||
"Expected Java version {major_version}, and found {version} at {}",
|
||||
path.display()
|
||||
);
|
||||
Ok(version == major_version)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,13 @@ use crate::{minecraft_skins::SkinSource, state::MinecraftSkinVariant};
|
||||
|
||||
use super::super::super::Skin;
|
||||
|
||||
/// A list of default Minecraft skins to make available to the user.
|
||||
///
|
||||
/// These skins were created by Mojang, and found by reverse engineering the
|
||||
/// behavior of the Minecraft launcher. The textures are publicly available at
|
||||
/// `https://textures.minecraft.net/texture/<texture_key>`.
|
||||
/// A list of default Minecraft skins to make available to the user, created by Mojang.
|
||||
pub static DEFAULT_SKINS: LazyLock<Vec<Skin>> = LazyLock::new(|| {
|
||||
//
|
||||
// The skins below are available in the vanilla Minecraft launcher, and were found
|
||||
// by reverse engineering the behavior of the Minecraft launcher. The textures are
|
||||
// publicly available at `https://textures.minecraft.net/texture/<texture_key>`.
|
||||
//
|
||||
vec![Skin {
|
||||
texture_key: Arc::from("46acd06e8483b176e8ea39fc12fe105eb3a2a4970f5100057e9d84d4b60bdfa7"),
|
||||
name: Some(Arc::from("Alex")),
|
||||
@@ -209,5 +210,320 @@ pub static DEFAULT_SKINS: LazyLock<Vec<Skin>> = LazyLock::new(|| {
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
//
|
||||
// The skins below come from free skin packs released by Mojang for Java Edition players.
|
||||
//
|
||||
// MINECON Earth 2017 skin pack
|
||||
// - https://web.archive.org/web/20210416010507/https://community-content-assets-cms.minecraft.net/upload/094a8f1a146ccb6573e08e0e35f9ac90-MINECON_Earth_2017_Skins.zip
|
||||
// - https://minecraft.wiki/w/MINECON_Earth_2017_Skin_Pack
|
||||
Skin {
|
||||
texture_key: Arc::from("6c25523e7dabfcaf0dbe32d90fd0c001d5d57ac66206a0595defe9be5947ff08"),
|
||||
name: Some(Arc::from("Globe Alex")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,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"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("66206c8f51d13d2d31c54696a58a3e8bcd1e5e7db9888d331d0753129324e4f1"),
|
||||
name: Some(Arc::from("Party Alex")),
|
||||
variant: MinecraftSkinVariant::Slim,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAB3RJTUUH4QoJEjAE4f2wWAAAABd0RVh0U29mdHdhcmUAR0xEUE5HIHZlciAzLjRxhaThAAAACHRwTkdHTEQzAAAAAEqAKR8AAAAEZ0FNQQAAsY8L/GEFAAAABmJLR0QA/wD/AP+gvaeTAAAFFUlEQVR4nO1aXWhcRRS+XeMmClmbNVtTMdjUqlHrX5QSC0oMUnzxoVFBEAqSh4oRBEEEH0ooIvjggyDiSx4EkUgfLC1IEC0+iFKoQluFFNpGqLbotqtNGyVtynW/ac/s2ZN77+z9nb1NPviy586dmdxzZubMzJlxJicnnSBW3x92JX9/+34tH95xpyJPgwwi3VS/bRacFtBR6W5iZ7mk5bzDaIAwSi7W5hXzhJZ6QBAGnn1OsfTw6DIivd0R2wB5x6oBbH+Abax4A3jO8ybyOT8uc7EO4MA01/P0eO6mOz8UaO5ulVCeELasF22jpR7w6J5Oxb5X3nM6N9zlbHjtM/V8PcDTAFAUBKDo3JEfnLF3ruj3kJGGd+fOLDTlzx1o08K5NLvXXZj5QHHp6K/q2ZP1dzpf/VnWQ5sivjmS6badoCM/GoRCFy/+pxUDFz/9SKWBkCmd8nrVw5WFLHeLbWkAalHestwIXHkygJ/yZIB23i4vS7jv4yGXs7+/v4mmCu9+c8QF/Qyy6d3NgTTVzxtn/vPX9W9UA6SyEvzp8c16l8gJ3FguBtIEzEJgsacjkW/N9VL4hvKAc6U2p36jItcGSAQYdxjrNAYnzvTp8f/U93eotMqLWzUpH5WhMc+JMSlpcpTSSVJd0hETLx/8cNn/4P4hlA+4XLukDXLgy9u1XD2yztdwvIzEY4d+Ud2SODezP7ARsJgCaHmMaFJW0EOAO6DKQ39FrrBj09X1/SNf79cOywsv9A5oPn9PtzPSXdChNMAtn438DWGgDcBbNKjlvcA/trtyWhkBfOOWV50HD+zW77jSUsHyvWedoZPfaeI5CxR+3jjicB7a+LJzdHSXImSkndg+pCnzA2tqvbrCC9XGEOLDCUpT75BlgNqx4Oe0UNiy+xuHSFj87YQigeeRJHDlRrefbpJfGr5VvUfvkHm5DPCW73liNgEVg9G0moBCN5cqTRn+na8GVvDPqWtD51RX/c8lZ61Tqrd84z3vBX//OHg1/7W8jXJdOs/5w6VmuRF+SAWx1wFr+4tNBJaOlzzpld/EtOFpgGK9FxRFTwgCd2jrx45rJ7jtrVktU7rMjzQ8g+jyXM4CiawEuUPjM4jfmoLnp54BcMeXmRP0SuzpXa8YBtyZkbMD+JqCp/NewB0fl6WDTAWDg4N6qwsZHH5mTJGe5XtOuZ2V22lJ03ZYEstaLHs5Kc1rKUy/keMBkmHjAXmj9Q+wTWOGFd8DrnsDxI0H2FYgLlOJB3hh4qYZFwxVKAMkHg/4Yt2+ZUpC8Xa9UJVIPIADix0YAazu/ET9QvmFY384W//sMleQNZIeU+Q/tuzp06QYH9Jtj3nJVAwAR0rKk0xGsa1w6gYgI3jRtrKZGSBPtP4Btmn9A2yzMD097XLu++pb9+BsVRGyfC9pdQpLAtjTwxJ8jy/jAV55eHqeacwQdjMU934BnRHi/C+J838TjTHB8fHxNZwZdMpMYTTA1NSUy5nFRxGSOP83YfV+gNzrUxzAL5DJnzGGZHla9tLS16scl3lQk8Y/fuV9AHl5K1UfwHeDct9vigPww1Eu83JBdeB+QJaXLn2HAI/hAzxe0MplpijA3YDbxnamUrcffA3AWw/wa0F5dO51vB5E2+iYeGBbY2pbqjN6MCiXiD0LJHW/wBYSnQah0JO79jaxHZQMwopfB6RigLD3C2xitQekEQ+Icr/AGuLGA+LeL5Bn/Pz8X94JiHL+b+L/USxRwWwDOUgAAAAASUVORK5CYII="
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("6acf91326bd116ce889e461ddb57e92ace07a8367dbd2d191075078fccc3c727"),
|
||||
name: Some(Arc::from("Cardboard Cosplayer")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAB3RJTUUH4QoJFhEoWLNhhAAAABd0RVh0U29mdHdhcmUAR0xEUE5HIHZlciAzLjRxhaThAAAACHRwTkdHTEQzAAAAAEqAKR8AAAAEZ0FNQQAAsY8L/GEFAAAABmJLR0QA/wD/AP+gvaeTAAAEzUlEQVR4nO1aSWsUQRTumbgkZNNER4NoQtCb2yFicL+p4IIIxoOgglFBvCgoihsK+gfcTiESQUUQT+pBFJGoONGLFzdMcCExQ0CjIVGJJa/wNdVvqqump6qneyAffFDd9erN1OuqV6/qlcMYc1SsmNLAVOzY2KykTr+OnZcOMCQ8P+84wkSa6k86RYiBnj7OPz9/GesqSgMAqqZMtqKnKA1gq/OA2Btgxtq9nGEh9gYIG1oDTKyozqsuLLx51Ol87e62p1C3TNQ2LGBAWPJk5UIsgx27VnDSOtm7oHRUnculLFImo4sj7p7Z6eHjdNolrQuDPNAZX1rh/qFCl28d2uShaAB8h1/L7zkfYvtx9iaTHWRuni3I7zy5fJDxAnyFKBn0q5l+eUqPAe7vWcVJ/2RPz1u2fU6NWw9leAdOLpf2KoqxPiXMUXEfINsjiDI6HbK6rGWwt2/E89y2eq7z/txuZ+/WpU55qpITyvAO6/3aFgOS0AEkdkB8J6L5dDsnhSgLOsT2OkaNQJHgsxM7OG1ifMVEX6451pbAsl87UUbG4R/flHVZgUHljEYmsmXaQg91gUXVhlXMhEH1o9/JN/CSjoCy2pTLfHAhMSJlHDG2GYr6D9hE3fTS4I1gLabzPjW/2SVdt6msbB77bYxM/UMYPiXwCBgZ6NfK7GOl/GuIhHf5gKVGPXRmf/RSJ6+B0gDDOXSW/iBy06iXfnJRI3abIYqLmT+e57qSWZ7nXrq6ZILpj70TpFNJV6+Tp4i9AcJG7A0AewuRtuWlBgDnl4sDjAKwG7WJ5IPHD51Xnfc8HPyc5oTyu9cvXYIs8M7tGy6LHXwEfMp8d0mhqpMhUfVFSQq/5REpc2p4LiEbDXk5wZlTq11SqOpkHS5JTVBSZ5BCI3l+XatzePEWl+0tm53ra9dzQlmsA1nKqDDU/8OOIl2sHPQ8IGzCnQA8zwOa6ov9MqgCP9ExRFEboKxykrmSYpwCNq/IOJh4xPNzXQOUVyUsRZ269jqK5/jYafFcn+qnuT+tATBDAsK5WBT+lCxbgxTrZAalHQQ5qlOl3zaTMI+szKX/QH2oE3JwyKutK3k+rrymlhMhlgsOcWjlMgLwy/qlrGAk4fCXUTViohgB1hWiEf3ycVTeT47Of+oH/D5YYB8QhgFUycpcDYB1tENUf6wNIMvM0qHtZ4CuKycZNYBsdJkaIJRACHNvtQ3T3fKHF13KHB/l3+Rvqbx1wFUUyPWLZGyAk74Xr68gqUVlcQJd9mTOTRdfhEXrCmWBkDj0Meag7Qrt/UMzgJ8zE/2CjV2cNQPQWP/U4nns2polnFCm9bq9gc7rx41agaCbIZo73Ll/0ENde788I94D0LXHEYYxg07e+ipgerfAFOIZwaJtZxM6+aI+D5BB3Nekrx5lOvnY5wbhZDfI7TOIPQLB1InQ+wPi3QLqD3K9X2CS/xejT/H8AB101l3hKA0Az1mdOtDoYeWu5Uqa/n/rPiCuKTU/RO4D6O2xuiGS/8+Ee7ss8lWAprLEtBfQNP+vQ+QGiBpjBghDaZzvF2TB9DzAtD3G+Minx9d7SOspjZfBZU1Nifr6OR46Tg0nfQ+ylGBEm/cLCg0rU8DkfkHkMD0PMG0vmwLiVAh7CvwDe09qkP5ikt8AAAAASUVORK5CYII="
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("b9f7facdca2bf4772fa168e1c3cf7b020124eb1fc82118307d426da1b88c32c5"),
|
||||
name: Some(Arc::from("Creeper Cosplayer")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,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"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("b7393199a84eb9e932efa8dda6829423875eb65af76cb82912ade62f93996b9c"),
|
||||
name: Some(Arc::from("Creeper Piñata Cosplayer")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAB3RJTUUH4QoKDhoN8MMxLgAAABd0RVh0U29mdHdhcmUAR0xEUE5HIHZlciAzLjRxhaThAAAACHRwTkdHTEQzAAAAAEqAKR8AAAAEZ0FNQQAAsY8L/GEFAAAABmJLR0QA/wD/AP+gvaeTAAADtklEQVR4nO1aPWgUQRTeW4IX40+MUSNqGjVVNKiFFmKRxiZgkVqwCAjWahRsDqwsg0QrOwsrsRHBziKICBYSQTiLwAXJJcaf4E+iOU/fyDtm5/bm7czbuZ29+MHHN7c3M+x793Zm37sJSqVSoOOuvYfqHFLzU/xRulkHLtw/HlFk7/BVLan5wyAHWD/9uEm/HH4oyEUXewbHEEZWg2DLQFF8lrVrZuxva4U1v/cR0PtuvGG0jG/VtY0ZAYj/EZBSBHjvADASjAXICk4B53DhvQPASOC/cA+alA3uPs8l7OXLExcb+3q725lHwPdKRdu+FdQKwNuD+wp4Hdp4PckcunbmDgAMnpgQNwUK6N99tnFdBl5Xx6pjWs0Vdz1zB+ANtdJW/U3m0GrWa0DP9IvExPd3kzEUM48AQHF2ldTJpScF7A/tJGOSaOYOSHqjnLE6zdwBgPDVrFYRU9XROtBkLDm3mh/P3zhX15HKr6n8vHy+T0tqfvV+Fu9eFkx6f7msB1D4OV+2HtsRDth0YMh6bEc4ACPAyhHUM0+x79KzCPtPTWup9qfIvb+2rAHPj45FdObCtWD961ysqn0pdQ6uh9/e2RohVG1/11YFsQ9+hu/U/hRzEQFx2LN/ONg8ckYQP3uJNCIAf1lQIO7P8l4Nit+rY3TqfQTs2D4UURnH7r0RpMZQ6hKpPAKfV8oRBeDWdL17Z+RFRe1LqXOkvQjKj4JK0wUwF48AYGDuYESLr2viF1QVvlf7UuoabAfYGOSTE1KJgA/vK9o29b2uDYBXXCRurbi9ctFwAL5Hq0ohqRFcJ8iofVxIdG+JoC4KnyZH6jJN82vXVHMF7ny5zAbXjnQLpoFYB8Q9ux0LNSRMS1R5fwSEwTCRrbbbYFNS84cntz0SkWCr7YBpDcGkpiDWAFxQbNUlXBoPCH02vh0QEbC89DTgqGuYps9G6bR86NCGrhfBVpllUpKLIDgBDwzYqmuY1hBMagrhRjYeICLg1+jLgKOuYZo+G6XTrp8xLjEhs9VEa4DrMONgqme8wFESUHcDT9hq3tl0gVsP4BYpW43DcwDUeONcIO2QdQWTMwAm9YImB/haC+CcAdDB+4IIGm4SAXD4SXe4KgJOLQCU+8cFl9x6gYiAPNQEWoGbLndEOsxxgvcFEdcIfa8FUODWCnJREKHAcYL36TAFbh6Ti4IIBY4TQt9rARS4tYJcFER0gLQXjLHV0LdagPz/v3wuQAdOzSB8sHilAMbYahxszxogUv3/n8AfTy+dVTxL25AAAAAASUVORK5CYII="
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("7cbe449d9d37c111a07a902e322d3869d98790c48f1fa16a24bcbe2d8d73808b"),
|
||||
name: Some(Arc::from("Sheep Cosplayer")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAB3RJTUUH4QoJCg0vNZzzbgAAABd0RVh0U29mdHdhcmUAR0xEUE5HIHZlciAzLjRxhaThAAAACHRwTkdHTEQzAAAAAEqAKR8AAAAEZ0FNQQAAsY8L/GEFAAAABmJLR0QA/wD/AP+gvaeTAAAEO0lEQVR4nO1bT0hUQRh/biuiQkmKlGKmBxchspOXCOrWuQikIKs9tBhdukQQ7EZgUF3zIkJ1COpkhyQwKBWjPweP4QaWREF/hAxSF4PXflOzzPt23sx782f3va0f/Nh5M/O+9943M9/M982sk8vlHBF7Hk67IrZfuiKkTL6MXS8+uCLqPj/hGMCXa1kuTWFbQxPh4Pa2UhpIMdLg1vEYRLYRBVQKiz/XjMuMhQJSzU2B8lQQCwXwWt5Ub4iFAgDQ4ixNITYKsIWkCSHF6caEGGWMFepc5Ztl83wlCPP5jpl8Gelcv2dhhUtazrsXGOTZkVCA3wfQD/QDqwCeIlj5sVIA+1HAzdsjXKr0AJwfSSO4WVj3XNefGnOSw7dK15CGPBU0fl/2XEdSARjF1nZ+3TlXuoY05PmhvqFRKnO9pZv8Rk4BvJenrZ/sOELI5vGAexAPtCdETgHCl5+d+0MF0BYvQ7UNoMrsoDLd+c4C2D9eTB9yRdSNH8gok48/Puz7WYkHYOy9fJ5LG+hO7SRUReRsQKVRUwpQ6gl4DG1cP+6hzCbwxvHS8jtCKoNe2zSEfqyKDejZtdsZHxhytrS3EEIa8qIII+4wRuHGCWd06ljpenSql+Q5qdM2HqcFqzZg8uoDwijDSg/oh5b+WkxkBsj1BUinBmw8ShsJsJy9B/pLpOOWki2jcy7LuIM7BJJ9vYT/AmpqHaCC/wqo9gvIMHP3rIfvX+ZCUQYrs4BpUGMLRpm1Tb/yS87S3BvxzV19wuLI9wA/5CcemREk8+fvdwx6qBsPCOuvY+LzAbryYtcD6DkBoAnETgGmEUsFwNa4scMSMv/bZFxPNR7gtz/od24odDwAQsaUEJdnyZZR4OtKA58PYM8LhQZudVbbWJu4l/j1Gj9tizYv/cLdqj0AyqsWEbKBsKdCvv1YCVTPmALYYTNxOO3yyNYJCxsnxADWegB7nk9rjCqgbWtr8Mq6Kyl2nLKEMXg0v07IG6+qNoDKMzULxMIZgg3T1b/pV4U1T74uEgf373MzmTMeZrNZQpwPdTFFwk0sWDJP79UB048nykjLRJQqQOvtBFgtthTLyOLZ9KTL8nVrq/ups5MQ0rgcU3UHx9RusS4dcHHhQ4MQu8ZB3OOosyxDNx6ge74AW3Td+2X1Y7MSDAMaLwiy/qhJBYRBTSog1PSrGw/QPV8g+0+QjNhzxZ4j7+gty1jGA4JA5j1S+8AdAtVwYEyDHQI854guzmrCBshaWxQbqAkF6Pgb2gqIy/kCvxhBcnhhvugxzYcUx9Rvcso8Qrp/B3t31QQMDdr94Zd7ELvSL2Ua1JjR2IBf7ICtw8JqPKAS4MUGwsQMqm4E385eFPLJ+EkPcbk2dOMBsNrD/+Vxn98kxPl4lchbKbL3UxmiVaWuO5z4PDzqNA+lSwR83NggBLBlUBfTBozt/QfAb8Cc/0AUtn6wAAAAAElFTkSuQmCC"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("b182ad5783a343be3e202ac35902270a8d31042fdfd48b849fc99a55a1b60a91"),
|
||||
name: Some(Arc::from("Cake Steve")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAB3RJTUUH4QoKDhAf+ZWo7AAAABd0RVh0U29mdHdhcmUAR0xEUE5HIHZlciAzLjRxhaThAAAACHRwTkdHTEQzAAAAAEqAKR8AAAAEZ0FNQQAAsY8L/GEFAAAABmJLR0QA/wD/AP+gvaeTAAADsUlEQVR4nO2aT0gUURzHf24rskGbhyBRyz/gwQ4pJAmBlQdNOkRGeSqS/hBhUR2SyEOLBEGBt06dorpYVCerS4JQdopMKFIIBRNDIjXKXVZ4+Rt3Zt+8nXmz7nu7v1nWL3zY2ffe/Pi937z329mZH0QiEZBxaV89U8HLvheMRZm7osr2PQfQB2BVvxeZ9WmSUGEEYFXxb1OM1dSwkRCsHSeCoGo/AHmi4LlTAJWV0NLUAqON1drs5k0ADJWXGx8tZTXaTOZPAEZGAGZnAWZmAD5+AigN67GbFzmAS3qiCiAHxNyv9sKSunnVK6zK2u98NHGVo+549Wd4rn8C4Oh08oZHZZKyceRbYGVizgAWYklQpSWpgxP9OP7ni2E7w+9t3202eZl2E3ag/3ATo0RMSnPPX3E3Osmrhm3Yl0mis+wlwO9mHzy60MGG+7ttYJsT4jgv3OzwyJzmUc32bqxFx2GvYLR5Mkk+4iScyNbE0g5AJlc2FysgZwEws7HuvZ2rGyXlAIgN4c47jGeg75oNL4PtZQEmQ7Qv4mX/15OrzORzX+e6zxfJ+c/gnt79UtLVjy/TWvxJCUCmDuVaFbuqtNghvxHKVLpWgLHnQ5GXFq2j72xgm2zPinv8bO0mKV45QgT3Ob/vxRzw9mJrCthukvUcsGNzkY26LQEp4ngvsq2sbIHzvT2O+FGB6skVaBz6Y2Cq6EbMIHrlO9Q9Gwcc44Yo2UT9GATbCsAg4MRVFe66B68fd1nf8Rjb/ChtW6DnyAHreGnwOnScHITQ1IQBHmOb01hqBXUa4yf24O59a8nzx36aPEp5BeCE3CaFE0dk58kCkotgKQfg4HSbjbbgXoPBmw9tmO3ieORpw21HsK+5+DQcrz3hCPYdnWtPAdtNsh4AVLyh3mLrziop/Fg/KLBt/hZsn+222L18yKIZzgD2y8h35cV/gQ9v5h1xU/HYVwtP5erBg18hd4AacgeoIXeAGnIHqCF3gBpyB6ghd4AacgeoIXeAGnIHqCF3gBpyB6ghd4AacgeoIXeAGu0PRAbGShhPOL5ow+v8Y/ExdrluwUK0p9tfXz4R0vbmNw1pD4CO+gJd7/7TUcGvAOUkwtcKuNUXmDjVFzi925fVA/DgeN8lwXxTwQcgqGrAViMwOQ7RoWoIhSqspsblZN3B33+p9QRQpuqBmgp+BWwEgNoBam0EgNoBp3f7snoAHhyvKvIAUEs5AGK9ANYU8DUGfO2BH+sLfLkCZPUA63r3n4b+A54o2ypxtYV5AAAAAElFTkSuQmCC"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("c05e396bbf744082122f77b7277af390d11d2d4e93dd2f8c67942ca9626db24d"),
|
||||
name: Some(Arc::from("Party Steve")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAB3RJTUUH4QoKDhIdJa2rQgAAABd0RVh0U29mdHdhcmUAR0xEUE5HIHZlciAzLjRxhaThAAAACHRwTkdHTEQzAAAAAEqAKR8AAAAEZ0FNQQAAsY8L/GEFAAAABmJLR0QA/wD/AP+gvaeTAAAEGklEQVR4nO2bX0gUQRzH10uvtCQVzQz7o1H2h+jsIZECMQiil7KHIurNh8oKhEqCiq5UKB9CiEIKCgwRfNLon2RBglTQSwWRBZWoGGZq3ClySFu/gVlm5mZn7txdZ1fvCx+dnf3tMPNzZmdn5qcWDAY1EZWFC3QRL0+UC5GVrxqfFoPWpftM8bq83wKLmrEDvob+IryuGTsgMQTmiBJDYKYPkkOgpbObiyckm+cx17ammELeZ9MyPPEd0FB72ujyPEBmabfLNzipayKuXzymlVx+pL3yFSBHAJAGQLLnZaiWsAfUVJRpSQv9WlfXEwQWvgZngI2XRTngYVsDAis7K8NIHz3cxk2TNuzzXlBUD/jc2RNllN5zW+s4NaKl/viCgDTkxfKs20U5ADegoXIfAqvm0k0ttKNKGz7SiIA05GGR9l5zgg/GMNtgnmp3baQQCZcHZctQLnZe3F58XCcJZCRRyObVZ90vdBGBQECIrPxvfUM6yYcLFWjfAX4DjnwHxKv1qzdxsUOZWZkGdijKAf5F+QZWFL5TRcG1CYcRKjXvV4NRY35naZ0BXG8u3EtRvOEQBW+cj4WmjDGJgTye7f3HHUK6371Hz/KAdwCvTHgGY/s7IBIJUddpadlRgFaeb9XGK4IISJvZ5qUvEeK0HBkCH3t7Efk5yxD42o2y3QFbiooM8JuazHOb5v1L0DUOGAqFKWZLrnHA2tw1FKTg/TE2OhYFiHUcMDk5YiDV0nM39NRgu0HarbcU5D2wZSGnHN6UBVMVec3a86YuNt/RaTCyuFBLyskzKNk2bQAi74EtC+lt0uP4rzTwa5j6q5nZm+U7rWSrBTQe3G2kq9ueo0bWl/M/o3lbYD+nxNtizSf3a/2C+yv+MzI6TuVNkxe5khUnHgJ7lvsQsFN7ryQFAWmcTw4BvDKEtOhLkgdrz35ZsrBflL9bqinwahDD2kuHQOmDs1pZ0wHKKd8ndAQpsAHbgrtnjDwyzSoyNUBhtwY/9Znem574E3tBMg/Fux/gNRIOSDgg4QD7Cy1/3aMDZtduwjEHwLSJG47Tqhs7qw6Y1z3ASyivgGqUV0A1yiugGuUVUI3yCqhGeQVUY+wJvmm6qgP9zbX6qqf1CEjjfHIF2d54RQdiX3O6Vz5eA3n7AdiObPiccIKsi8S7GLIaX8A7/ydjABwbAm6S3TEAIjniALtiDGZDruwBsyqrY4iNH+DFGIh2gdlDDfYgRXao4ul3ABtroELzfggkHKC6AqqVcIDqCvACI3ixADjGiDxBtiWQwuo0YjW+wPPTIBtfACJjDGTxBaqVrLoC/tY6Iw3/fAGd2qxjw79mkLEAfvghO/+XCXcFWXwA2W3IlZ3V+AL2bJ93/i/C8hCwuh/gRHyB6OwfK64YAIH+AUWxpRxku8ezAAAAAElFTkSuQmCC"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
// Builders & Biomes: Farmer's Market skin pack
|
||||
// - https://www.minecraft.net/content/dam/games/minecraft/software/farmers-market-skin-files.zip
|
||||
// - https://minecraft.wiki/w/Builders_%26_Biomes_(skin_pack)
|
||||
Skin {
|
||||
texture_key: Arc::from("2007b66a99ae905c81f339e2a0a4bf4b99e9454a485d5164e3e1051c3036ad70"),
|
||||
name: Some(Arc::from("Barn Builder")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAABGdBTUEAALGPC/xhBQAAAAFzUkdCAK7OHOkAAAAJcEhZcwAADsMAAA7DAcdvqGQAAANDSURBVHja7Zq9ihRBEMd7j8N8BRODFYUVUcRDcNFAMFoTA8HASL1IE4NNxMzQFxDEZzARcx/A/B7ATDC4yEAQpZ1/s6U1ZX/3zO7MXi/82Zn+mOn6TXX3TFer7x9u6RvqncZvNpspi3ShlE/NbbcqdfT2qr4weaKfv/cDUEoZ7RyAjy9P6/39S3pv76n+day0D4DPUFyHazQAPj+6aRo5mdzVX49OIIBQA2O7wNgB6MVikT0GSOMFhOEDWBsW9IAU4xmE3ekCuwogajrLnAKDGg2AvjQaAHhhqgA8+ZhFjr99Mu8SkH71Y9wAYDCEgcy0shkDKA3H+CGP0mS90XsAGYI3RYjOXek7C4AL3wyQLS93jBgVgJBGCeDh5bMtnT/8orlkg6fTaat8aP2AXoiuv/ht/ufzeUjq/sGBUxcfXNMQXV/Wh1Gre+e2D0C+EVYAFcBAAaBhJBjMz21p/NwGCEKeHCCRRo2XQp5rkHTVCQ2uVC4agM14HwRKs92Yl8UbIh37ALiMM/nPbhvFAuDlkjwgR7anzw3H/2q1+ldnbUxLlvS/17OU90Hg+TjOAsCfcFAWY+Q40rpeJABn2UT17gExXSa1i/G0ZkAzSjGa6kBBAIdvfuoSdQHAp9ePr7QMihXqQUEANH2gr5KWy6URT3NNW76n+p/7Z4gMcg2gNiV5QMS87JWv8VgXQLClpDvxAS1WSbNAqQeAMp4wp87PASFURp7zY1qOz1XvHuAa8SFaHfKVCam0fckAbB6Q2wW6ELWJ/3cKAG4iAUhRns3FupgyQx5gAyDbnQ3A9+HR3NDIlY+PocaIIjVGBkXtIOG+9JMfc1IVQAmAkEI334SKl8QqgAqgAqgAKoAKoAKoACqACqACqAAqgEahCgg8+gCkBlel5AqvLXhaAVQAJxjAnTOnugWQWkFuPijdX5AS8XFoGABy9xe44v+t/QP+/M0CkCpe+/eEtkKhr/Xx8AAU7S9I1GC6QJceEIr5iz0D/QCAoVI2AKX7C3Ji/mLPwOYAcBCk0uhyTsyf7xnoDUCsivcXZMT8+SA4GAC5HlAa/1dKjdsDSjVIACn7C0IK7Q3YOgBbnD5lf4GrW8QCwMdRSfv/AAtFWPNdllX9AAAAAElFTkSuQmCC"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("59f2872323bf515aa8d84c00931fbf8170b2cec5138961527c09ffcd06ca4ab2"),
|
||||
name: Some(Arc::from("Bee-Friender")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAFaUlEQVR4Xu2aX2wURRzHp82Vu0LLWWpr4YTGouGFVCQ0sSRNjNVoxBjFF7HxQfkTUB4QKSEQHwkmVeRF0lTFp2qMgkaNwWgNkQT6gKIEDSSgaetVaLm2V6h315Ke+e3mtzczNztze7vu7R47L7e38/f3me9v/uxMBVGE51fem5Ul+TMxKS3h/NRMhaqOUsYrG3fHA0gPrJQqYMMLN6QdODCeVEL2tQKmpm+WN4DspTapAsa+jksB3NM96m8FhEKh7NzFh4RGVq0+T+KHGgMAMgK+V0DZuwBOc0e3z5NXeytJf1/Y6NCaJ4a053Xf/kXOrtjIdHT78Aly7qn7tHe3vms24rq2ZQiWBS+9PkhWFAIADOHHAfB/DCoAE3O3hV6ypCpESj1NugYAjKUDQPE8AL7bDm6d1l4deH+x6bgncgE0FpUAhvsGQPKXFGncEydjb8dI9Nl3NMOTX76Re7e2moFRngA6vieZ+guMoeFEKxk7/TiJWgDgSxcABWDPX+s9pdnQtP0RQwmFAhD5jC/GABoAbwS4QiEAZAulks8CfOP47S+/31ft799saZDuHXbuqLK1dN6wtI4p//hPR0l72xajTFX7+MrzNio8gH+mkkyeM4mb0s2NVQCJ8Xmt/PqGSu1XtXTmAYymZpn2OQ7gjleA1wHYVsD1nmWMT4Ek44mUIaveU7XGM7jDB5sXGXKFCNg/8CFzW7z0DYfY1SDms5peOohwkcev/i112QoRAPRHgIHPWC7/zgkAPAgzUFYMx7RKAJuW6aPqkdf1FR1tIP383FviT19L74qatgt7ljbo3DXxN8R1TXcz5WA6/r0KAuSj8ygB7GlpzMbTc6blDmVycu6MRsjv/86RcGVOVWeSOXehCzEzCNJc/irG1LfqGfFnNUhnFqcCgfFDM2m5CwAAurBqkiUNy2u0V+Mjt/Lq4QGgcjAhLIxkBpk1vL9Hh9DVzcIp1FCzdJHOq9YBXEnrvX5/JH/QKgQANsaKUeH2QXJs43It6ysnRkjm7MN2bdfyFwXArgKgYqvGg8EA4YcbdWR4W41jEFwHAMZ/MzDByBkMAwNhX8GHxtd+M3qbBoBKOPTo2rw8uw8uKEgdhw/Mkv2DI+66ABr/dOcSrZFoIBg39t6DQgDwEuIgLSig9dMV2jOEkgCw4gKPtaYIGssbj9aCMWg8v3ukwdAAEJ4IAMSpVAC9D0GpgPX1tdossD6qrwNks8BAMp3Xg5vb2Cm0mFEcIEDAgY/+j4YUpHlBooIBFFvBF/tqDZ8HJVy4NKMV1fEiu7A5/bH8ENWs/o59vaTryW7Sf7JH+9UG2JM95PBLe4VZfh5nd+PKhVCxhpdLPk8fXLoBOQDgBmUv1xEowMu940bbAgW4QdnLdQQK8HLvuNG2QAFuUPZyHYECvNw7brQtUIAblL1ch+MK+GNvE/NFoquPPVFSHV83L4pkVy/O3Tu6OJ37kHr5149I5IFNjrbZ0cKgp3kAWz7UvxBhUN0vkAGAMlQnPVbV5jgAuzdMfK+A/xOAJxXAH6/jR1FobKy+Ou94nZYonD4f+1y/IoPhs+vTJBbJ3SOqW8Aez03O5g5r4VDXrkvYdgGr9wvKDgCeLnfv0HvK7H7Brnf10Zw+bof/eB7hWwXwx+uyuwYgbTo+M58lLQvZcz7fuYDqfkGKsF6mAnBkeILEwuK7RKIpruRjgAgAfb/AKgBeQfSACAD4+E9GJ22NY7YyQ4OcVkAAgLqvBAOmCEgz5SKeVIBTLsDPGDgGeB4Afb+g2DHAzHgeQskVoLpfgADMrtNZ3bzw6VWbK1X5tgdBBKCqyEr8j4N9ZNWal40ssA2G//hLl2V3GvwPdSK8bg7in/sAAAAASUVORK5CYII="
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("7cd85127cbc710a1c9a53c6bb3474f59995c222b9d8c57b293993cc2d8a225aa"),
|
||||
name: Some(Arc::from("Bee-Friender (Alternate)")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAFW0lEQVR4Xu2bW2gcVRjHv41Js2k2jbnaZk2raVGEENFaaAtCIYKgIrQqXvLUC8WoDwqN+tJnlQrti1qK1qcqpaUFFUFwQRBbwdZiSbWKtiZhe0k2l93msuvajHwzfLPnnJ05Z2dn7O5J57xkds6ZM+f7nf93LnO+RECRnll7tyErcmlyWlrDuZm5iOodlcxXNu62B5BNrJUq4MkXUtIOTEyklZC1VsBM5sbSBmBc3CBVwPiXSSmAu4au6K2A2tpaIz/8kKORdb3nIPlOZwhARkB7BSx5F6Bp7sOXF+GVgzVw5FC93aGxx0fM60e+vgynV2/jOnrT6Ak488S95r3Zb9bYeQO7c0B14c1qHyQjpQBAQ8RxAP2fkgrAVP5fRy9prauFSk+TtwwAGssmhFL1AMRuO/qe1ZPPv8Ubw5ZzcgEylpSAhmsDIP3zAnTuScL4+3H46HiNaevgs4v2veaHGzhOSxIAGS4qAkF4AaClC6ACCMDYH3OmDd33NdpKKBWA0yioxRjAAihXAbKFUsVnAbFx4vZX3O+r9vd7ezqke4fXBut8LZ3Xd7Rw9Q/2tcD+n67ZdV7IzHvaexQVFgFcnUlzDT41eUP6Aq8AJicWzfrbOqwBVrV0FgFkczmufYEDuO0VUO0AfCvg+r4uzqdQksnJBVtWB79rsq/RHT7e2WjLFTNw/8CmvzPWTOGW7llhzSCUVOWllQGAqr6zE9NSl404ASB/RBh0TQ0R72kP4MUua1Q98Ia1omMNZK+3vuv86WvVnc2eejQ1X1AX+2D7cn5FSeXE+6IiRAWcuZYC9hmlAvb0dBrJbN5VaSO5wk6uvzkKF+bzUF9TUNWptDeD8EW/fxHn3nf/086f1bCcW57KNSh/ZC4rdwEEwFbWAAZ0dMfMWxNjs0XvEQGQcqggrgxlBrk1/Mg+C8LAEA+nVEPdykX7//IO4M+s1evrosW7vlIAUGO8GFW/6Uc4vK3bfHTHiTHInd7o13bz+bIA+FUAvtir8WgwQvg21QKju2OBQbjlAND4rxJTnJzRMDQQ9xVi6nz1F7u3WQCkhN6Vhc9t9OzZk6tKUsf6rVdBtTKMOI0BflyAjH+qv9VsJBmIxo1/8KAjALyJeVgWFdB3dLV5jakiALy4wGN9C0DGisaTtWgMGS9un1kwLACC5wQA81QqwN7HpFTA5rYmcxbY3GzNw7JZIJHOFvXgzg38FFrOKI4QMNHAx/4mQ0rSvEMhNwCGYWyBSAQiBKDcF5x8u8n2eVTC+YvWUvjRl9q5Kr//TH6I6vb+H4ab4NPhFGzvbTf/YsLrY79Z44wquQIA2ILzo6e9s+plOuaHAHTstSDbHCogSJo61hUqQMdeC7LNoQKCpKljXaECdOy1INscKiBImjrWFSpAx14Lss2hAoKkiXX9+uZK7pxh4BB/oqSKL1jTGDV6VxROiYYzhQ+pezd2wa7EpUA7LdDKnADs+oQ/LFXFF8gAYP2qkx6vHRo4AL8RJtor4P8EUJUKEI/X6aMoNjbe1lB0vM5KFE+fDx+3QmQoHbuegXi0EEfUsow/npv+p3BYi4e6fl3Ctwt4jS9YcgDoZGlo0Oopt/iC1/dbozl73I6/6TxCWwWIR2uyWAOUNpufWzSgZ/kyvV1AFV+wIBw9qAAcGJ2CeL17MLY4zVV8DFAdrnoFICqIHRDReDH/8yvyICjVusD3IBi0AkIATLzS+dkczN7kp8nYHTXQFyv8W09VKoCNL/DjAgjAKVU9ADa+oFwAbsYTEIJQcQWo4gsIgFs4XTp/UzVOSfNVARCqyn0Pgn7jC9wAPPdAKxcPIMYIkGF+p8H/AOqQyO1eub4oAAAAAElFTkSuQmCC"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("5e4e09eccbce11e701c51bb64b102d688a6ac4018c725dd2b780210aee101b31"),
|
||||
name: Some(Arc::from("Buff Butcher")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAABMxJREFUeJztml9sFEUcx7939q6a1q3a657HtUQCwSiXVDAQSlpj0sQIPpAQaNBGIcQE478EGtIHlcaKD0TltLUxfdAgjcYUUp+M5aFBS2NMSmgvAYxaNLZ3SJcWuYPG9IrUB7p7O7Oz/27nbk/Zz9PM7Pz5zW9/Mzs7v58PJmzfuGLR6PmeDacM22/peshnNoab+N0WwG3KnHYwONnCQw7XuOMtwFOA2wK4jacAtwVwG5/Zd77QnPjxd1fPCUW1gLpICHWRUDGHNKUss1AcHdTeF1TSQsXdSF7LFmVcM3w/H81aWgKJ0U2oX/9D3gMd7qtH+/MJTfrh3cHSWwKJ0U1EfvWuALoGf9WUG7X5r6CxAPVE5DfOmpzaGlhtrOK2BRD/AvRE6fxg/yUAwNMty5jP5TInS6XYKBbA24StKsFtC9AMvvnlPmJJXD/5KvF85GLaUOANz368KF7ohPToQaJcLlsbW2koUO8bmw373/vut4R8W2JZfPJOm5IfPDNhS6GOf4f12Pj3MSL/W4HGEUURQvBW3u01Cghf6CTy1/Pumo3w/Utc+5MkCZls/meZgllAsXBqAb71O95fBIAn092GFb+rek1JR+aH8Wf5EwCAUE2YqLc8GkKqfxdeaFxOlB8bmUS05XNMpmY09QFgf7SZKD+SGrI0gbPj54n8usfWEHmzPcUfqglrJmFEZH4Y1ZUCYoFxROaHdetlnznKTJcatpfAQu0O4NrJXFqH02NTaFqa+OmxKdN+j6SGcP6znQCANXu+Usqb1tbZko+u32tSX1HAuVWHmBVmrkxrymZvZAwnL2Nl4mo+fesKANL8g9/sNmxTniI3wOAle/tBXpsga/KxiTdvJyYAIXwPQO32cllsKa+n8GJTUl8BqxsfTwqigG1v59bwQMdO1IduIjFTUrpWKKs4e9Cwwj/UhUnFFLnG5taRBycAuNi9vWQnTMO8E2yLfwkA+GDfc6BvjIQAqYAXm6KOhQhXVxH56dm0kv7w1GVHfZv9GzCvxCRJAgDN5FllQwntTk+bfOMrXQCAkZ7XmUI01wNV294DAKQHDhB9CsG7jORnHoPtnAyZdtrTud9yB/JurnwFdBBFkdkOAJrRi/TAAeaze8c7dPt08g8gUyYEbuGn5F94pPZ+9iBLbzw1PQsAmnpzjDZ21/9Hc3t1n7W2xyGKIiRJUpQYb2sFcPtN00pQv/3W9jj6t261JYuHh4eHh4eHh4eHh4eHh8f/H+7BCU7jC2j//x9f53ySHd19aGho4CpzyV/dqm945LtKnnBXAO/4AvWVF32vyIM73gIcr6d84guMoP37Zpj5/80omgXoxSCwvM/FxPUlQL9xu/EAZv5/M7gpQDbxUE3Y1EmipnyUdHzY9e87pSAWcG7VIQSSxy0FUfySvAoAWF37gGG9E2NXuchGw10BgeRxVFcKQKWAWYtKAHKKuCyZW0BrexwA8MXhffkLugR3BViNIaKRLaAxylaA2gJ4TFzG8WfwqcdXEkdX2n1uxrIHa4i8ngL0aOk5U3qfQXV8gRmy6efyhZBIH+4KyCz4DeMLaPzU+7Pj2+fhHue+BOzi92lFsKqETNZvOzqcxrEFyHED0XA1AP09QC8G4cZNMgKE9u8DMIwPcMq/veWc6whwjEkAAAAASUVORK5CYII="
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("d66ed86ce96a1b63c30f1baac762f638717930866474ac4fce697cdbd0bd6fbb"),
|
||||
name: Some(Arc::from("Buff Butcher (Alternate)")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAABKJJREFUeJztm11oHFUUx/8bNwsSbLJ2Zjak1iCJNKQLSQpVUxMIRCoJiFrSUqXix0vEqA9ixH5gsSSpEKmYWjA+aDUIZZFVEJS0CBErCkVJIQ1+NIg2hWSyNVlKHlyV+BBmMvfOnXt3dr629v6e7pl75n6cOWfm7r1nYxAwuLt+jVf/eM293PvbMpmYqI8oqYh6AFET99rAbV2CJjJeewiWm94DpAGiHkDUSANEPYCoiYm+80Ezevb3SNcJoXpAU30KTfWpMLsUEl8qJELpqEm5xSxv3nQrfsr9G0q/IuIndu0tSjHZ14zlT2ZL7ui9X7J4VH1gvbychdHv6amRktv0A2YIJPuaSXl4GLveqLBd591zoxD789Ah4iVonYjxxFmTs3oD655iuX1kJNKXILGQtz15Sl79OQcAqNqmMOuNa15CJWzMECjGhau2KdzJu2mrXLC5X89zE0RIXJ98nqg/P5fnuuw9j72zps0eg978GrO+Ld3AHdD44R5u+/3DXxLj600X8OlbL5vy6alfXYWU55/DTjwRO0XIE2sDgfSjaRrURKHk+20GSM0eI+TrJTfNZtPXz/ranq7r8LKWCcwDwsKrB8R27n1zDQC68ie5ilPVL5jldOU0Zv5uBQAoKrm0vXOLgquZJ/H5ByeI6w89/RK27PsQf1zN2fQB4PjqN8T1g1WdRU3gx+lLhLyjdTshi94pFYqask2CR7pyGnVJBbu1eaQrpx31Pl5oZZbLDdchsFDzIOrwg1nm4WbiB6s6cen9/QCA7c+cMa93tm11NT5af1ygH6M/ezS5pUVCVtQUalcmHSefvnyE2+FM4xAhGyEAbISB1f3f7bnGbW/u+y8IueG+XkKO7TjADwFu6w6InvyNRFl9BYp98flJIAbY8/pGDGeP7keL8g8u5srK1iZxUczSiww1T35z6ZgGgLmTfWU7YRrmnuBdd9QCAH6bX7AbgFp0dLe4e0uzSG2uJuTFa3mzfObCIq3uCtFvA/aW2PwCAPvTZ1376uIVmw7t8h0DYwCA86deZA6iuwWo3jMKAMhnB4k2VcEqlzVGNytDpp+6WVsbISAKJU3TmPcBQDfGkc8OMut47fqxnxlXEwV8N7eC9oYabieXr+gAYNNjOajb+H97td+xrmNgDJqmQdd104jZo+sLJjVR4IZox8AYRs8+7GosEolEIpFIJBKJRCKRSP7/+J6e4jW/gD7//+vcxjZa//GP0N7e7uuYy37r1rrDo+u67+37bgC/8wusW170vqIf3PQe4DmeSskv4EGf74sQnf+LCM0DnHIQ6NPnsIk8BOgn7jYfQHT+L8I3Axgurqgp4SGJlcHkBCE31PY6aLI54ErbTiAeMNM4xE2isDKUuQAAOLJvJ1fvlVcP+zI2Gt8NULsyibqkAiQVYLk4IwAbhlA/+1aoKzprdIPvBnCTQ2TF8AA6xcXA6gF+TNzA82fwqa67iaWr25y9/kfuJ2QnAzghygESEcg7wJpfIMJwfRNaDhjfDbBUSHDzC0S48SA/jscD+dPUUiFR8uDC+g+TgWcPMPIGGreu/1BxeoJOOQii830A3PwAr/wHFAVwFsjUTFkAAAAASUVORK5CYII="
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("b9e9d1b51b4be289b9525d4decd798cb7912e920bac8846a2df70e9ff4f0b1d8"),
|
||||
name: Some(Arc::from("Homestead Healer")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAABQ5JREFUeJztm19oHEUcx7/XXh560Z6YtGfrNQ9J8R5aasxhoHkQ8iBVqQVBoUTtS44qtokh+BBta4Kx0YcYTdN7kRPB/uFAsViCpfShUrCF1CshGMrRViEc1NqesCG3eUg0PlxmszP7Z2Z39u42sB84svObndmZ3/zm99udmYTAYeWvmRV9WlVKiETrDen52VvYvKvNUD701J4Q7xm1JOxlZfOzt6i0mUL8BlcBqlLiylSlhHA8QcmWC3nJplUHrgL05g6YT4G+VJeh3OjQoAfNqzwbZCsw6/x6wpMpsJ6RngKWFTM+wa9UzALWy9QIkTivH1lyre/o2LFeqmD/yVPaNbmXLc9iJq/1e4KQExw71ou5B4+oH6sQO1SlBFUpYbmQ1679AlcBpKNNsUbqp88TIRKtx+ZdbUL+o5pYKsBslMjoi9y7XrB0gn2pLnyVOY/jp7/RZJ8e7QYASkZehEaHBqGi7PmXC3nt2iztJygFsCNJ5v7X1/7A1oFOEIPf+mUv/v78Kg6/0KxNB33YY0NgOJ4wvD36hVApf0P72utLdWkdMiP/+00AQGL385S8/+Qp2wjAysiXo6qUUJ/YW9MoEHYSr9mOy+AXKzBoX7kUob7/3zz3LpU/eXbMdsTaX/tuJZvsAQA0JJcAAMVcHQDgYG4CJM+K5uMKt359+tWNE1T+iR9uOrIoofUA0un9b/Wv8O4lfHb5SeDyWvrDff84aVfVkP4adMKVo/9V83FCCFmAk5G3omHvuC8/kELsnDejmKtDOrOII6lN2rwmtA8fNtwfjbXh2UdrCyKjQ4Pa8hjr/DoH7uDu9EXT5+5sPcDtgPKAXoaLxuhluKkLh2x9gqM1wXRmER8nxYqQcGr25rjjpS+0652tB7BlWxwP7xeoe7Zsiztpmmtse0O8NytjrcCM3u5Zqg79yLOdY0fNSlYJuE4wnVlEOrMo9RDyJmj2IUTMPxpro376PBI6rf6y8PKptvFuaHrMaAVec3f6osEq9FPiYG7C5i/tA4z5h2yfHTYzcx4iZfT36D+G2OVyMv9ZH0DyrEaRdPLSK98y8rL1kHLNF+zbaRoFiNcHjBYwt1Ce/yQiTI6olpXPXHsb2dtnMPIO0PX9DADg/Bt77FvEsP+jiEHGDgD7xqmXR19W3UUBK9N3MiWyt88I3+sEKycs4pxZXE0BQjFXh0/+7TPIH94v4LfuK7j646pg39q2WU/hdUch7peRn1y3r8yftrlhtx4+nSlrO/qceQhrSP5MydpPTAMoz2snIa5p46+u2rcGRwHzO542PjTWCLWlA5F7101fZNxQrbjulHBjh/F1k7g1taUDjS32FWQf58daQCwmm3Fu+hlX5QICAgICAgICAgICAgICAgKs8Px0hhfnC/Rp2f1/HlXZHp88OxbidbxWVPV8gAjVPkPg6X+MWOHF+YJKIW2WVjtLBHbZ/Uhqk3bdkFyizhdMDbcCAF48LW6YvP1/HhWzAKv9BiLXK4JqUDwB4E6lmmV8nmwFsjtLtUZaAe53llbLbTfPzyZ7tGN1dn9lkfYB47vrKR8wt7AkvIE6t7CEye3vaemp4VaE4wl0DohPAd/6AFHo/f3x1fMDG7g7SV6MPuADBei3tIs33gcAZJPGPD3FXJ3wAQgeNVeAHtH9fTfnAKyouQI+GIlJ1mC//c1DWgHkyAxPZoXd8XwxaqwA2fMFT8g2QBJpBcieL8C967JNkOJ/v4ATN/UXM+MAAAAASUVORK5CYII="
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("83e283ab33558baa2cd0184d2e85f090c795a797bdbcb2cc47230c27f23fe9b1"),
|
||||
name: Some(Arc::from("Pig Whisperer")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAACX0lEQVR42u2asUoEMRCG7wWuFqxVsLDxQOy0srMTrhNs7ER8gwML0QfQRhDsrMQXsBLEB7DwEa6xFgSjczKQG3ZnJpvd22R3Bn5udy+b2/kumSQ7GQwEG+9sOk6fT8esBrmbASAO743W+wkAHTcAfQMADqMoADjuPICPkzOHAqfxE48ldQpAFWU5zEHTRgemlxessByNDVoZgNQAUOc0APzz7ADcrWw50PT99f+TOPdXxIFRx8HgOwrArwv0vDZildQwByr7d78e7udU1gJofZKS6QLfL5OZygC8HRwWHlMAWE92MUACoG0B2QHAB0bhDC50FMD7aH2SkpsXYPBCjTeW5iTdf7W/6jjR+qmk+mkQBeg/k+FMtUy9DYAB6BkA6YEkUUCn28usQstLE6noidbN5MjFKHsAj7fnrkzgoHTNdwamwCDqMAfAB6EtS3+PK9MKAOnB61ItAGK7QGwMaVryVDjyX+D6n78a3L0eqvps2QpSs7KsFAO4oMT1VylIYfMEIQBUkgDQIX/M1gCQojoGRXA8ZhTIHoC2PK428XzhMSC0C5T9sN8F4Bxna9zD4hslAIDXpLxDkaC1qfMSsS1A258RAFcel9cAAK8VLaGxrrIlth9vGmkBVUYBreg9MQCSfN9gZmZmZmZmZmZmZmZmZmbWPYtNrtJ3hNltvTUABqBnABa9v4CqdQBN7S/gYPjftQ5Ak1Ln0u/avQJFr+s7CyBE2XeBxnN/TVuT+ws0GaYkAWizwZ0F4CdYiwKXAfCudTIGhHSBKvn/pGaGsS0gdHt9ctvtQ1sAVdsAfgGGgsd9iBy4ygAAAABJRU5ErkJggg=="
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("e1fc44f1d69fd2864df7b80618a38af4170d4800f2df4fbde81c17b74b2a818b"),
|
||||
name: Some(Arc::from("Pig Whisperer (Alternate)")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAACWElEQVR42u2asUoDQRCG8wKpBTtBRQsbIyFlQLBLFxAVBB9AxAcQAilEH0AbQbCzsrC3FR/AIo+QNxAL10xgZG+4m9nL3Xq7m/nh5y65vc3Nl925vdtttQQd9TuG8/frCetW7FIACkABLDeAg90tg4aAcYv7yQP4en4yaAgat7g/Ob9knRSAPEcPoLe9ZvKMAUxvrlljuaJ67C6UZwUQIgA7OBcAuB8lgMf1rrE9/XzPBDcrYkA0cBAcy0CanUvre9vssI6mBdDkl0wLwAu9GnbnLgLwMTzN3acA7HqiBICmSZBrAWB6fjQA6IUXAZDuAkX1SA5uXECT2OHOSsbS+beDDcOZ1k8t1T/ZHxjbMNz+GbXnrmXorQAUwJIBkC5IMgV00VtlXba8NLCSjosA7kdnpoqTA/DyMP5zUdB2GTuY+TB6ZhowB8AG4VqW/h5XphEA0oXXZS8A7CBdAFTNIb4tAqj6L3B90P7cv2s79dmyfbxyDuCSEtdfpSSFzROMANBBAsCA7Hu2CwApq2NShMCDvAv8FwDX8vi0WQTAew7w1QVwtMb9Y/B0CVsAgN8d9/cypk+TecehtTk/bVZtAa5J0BUAPFpLALAuDgDaSwtY5C7g2mepqwAI8n2DSqVSqVQqlUqlUqlUqvRU9+RqdEtvFYACWDIAPtcX5L2Zhu/sMo0D8LW+wJ4y56bRgwTATbHTY9JagaJjuJ8kgDIOFoDL2gL47H3uz7d8ri+oZfa3CQCus8HJArDv4xSEAhAAJJEDynSBReb/g1ptXrUFJAmgzBqDpgH8AkujGNVuwmDKAAAAAElFTkSuQmCC"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("25dc6421d47cad8e2bdf93f56fae9ab06fcfe218c8645c1775ae2e4563c065ad"),
|
||||
name: Some(Arc::from("Ranch Ranger")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAMRWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNi4wLWMwMDIgNzkuMTY0MzYwLCAyMDIwLzAyLzEzLTAxOjA3OjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIiB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxNS41IChXaW5kb3dzKSIgeG1wOkNyZWF0ZURhdGU9IjIwMTYtMDQtMjlUMTI6NTg6NTItMDc6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIwLTA2LTAxVDA5OjQ0OjUxLTA3OjAwIiB4bXA6TWV0YWRhdGFEYXRlPSIyMDIwLTA2LTAxVDA5OjQ0OjUxLTA3OjAwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjI3OTc5OTlhLWMwZTUtNjE0MS1hODY1LWY3ZjY1YzY0OWZjMiIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjcwMjI5NWQ5LTk3MTgtNjE0MC05NWY5LTk5NDY4OWY2YzhkZiIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOmZiMDU3OTFjLWY0NzUtNzQ0ZS04Y2ViLTU5OGQxMTUzYjVjYSIgdGlmZjpPcmllbnRhdGlvbj0iMSIgdGlmZjpYUmVzb2x1dGlvbj0iNzIwMDAwLzEwMDAwIiB0aWZmOllSZXNvbHV0aW9uPSI3MjAwMDAvMTAwMDAiIHRpZmY6UmVzb2x1dGlvblVuaXQ9IjIiIGV4aWY6Q29sb3JTcGFjZT0iMSIgZXhpZjpQaXhlbFhEaW1lbnNpb249IjY0IiBleGlmOlBpeGVsWURpbWVuc2lvbj0iNjQiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOmZiMDU3OTFjLWY0NzUtNzQ0ZS04Y2ViLTU5OGQxMTUzYjVjYSIgc3RFdnQ6d2hlbj0iMjAxNi0wNC0yOVQxMjo1ODo1Mi0wNzowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUuNSAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjhiNThhNzFkLTEzZGEtYzQ0NC1iMjY1LTNkYmM0MzZiM2EwOSIgc3RFdnQ6d2hlbj0iMjAyMC0wNS0xOFQxMzo0NTowNC0wNzowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDIxLjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjb252ZXJ0ZWQiIHN0RXZ0OnBhcmFtZXRlcnM9ImZyb20gaW1hZ2UvcG5nIHRvIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImRlcml2ZWQiIHN0RXZ0OnBhcmFtZXRlcnM9ImNvbnZlcnRlZCBmcm9tIGltYWdlL3BuZyB0byBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo1YjI0MjVkNC0yNmNiLThkNDgtODk3My0yYzdhNmZjOWRmYjkiIHN0RXZ0OndoZW49IjIwMjAtMDUtMThUMTM6NDU6MDQtMDc6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyMS4xIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6YTY3MjBiMDUtMjc3ZS0yYzQ5LWFiODItNTQzOGUyYmFiNWIwIiBzdEV2dDp3aGVuPSIyMDIwLTA2LTAxVDA5OjQ0OjUxLTA3OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjEuMSAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNvbnZlcnRlZCIgc3RFdnQ6cGFyYW1ldGVycz0iZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iZGVyaXZlZCIgc3RFdnQ6cGFyYW1ldGVycz0iY29udmVydGVkIGZyb20gYXBwbGljYXRpb24vdm5kLmFkb2JlLnBob3Rvc2hvcCB0byBpbWFnZS9wbmciLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjI3OTc5OTlhLWMwZTUtNjE0MS1hODY1LWY3ZjY1YzY0OWZjMiIgc3RFdnQ6d2hlbj0iMjAyMC0wNi0wMVQwOTo0NDo1MS0wNzowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDIxLjEgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDphNjcyMGIwNS0yNzdlLTJjNDktYWI4Mi01NDM4ZTJiYWI1YjAiIHN0UmVmOmRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDpiNmRjY2JlOS0zYWQ2LTBkNDEtYjEzNi1jYTk2Mzk5MjVlMGIiIHN0UmVmOm9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpmYjA1NzkxYy1mNDc1LTc0NGUtOGNlYi01OThkMTE1M2I1Y2EiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz5r3WCxAAAFnklEQVR4nO1aXUwcVRT+Boah+0OXwG6XDdgiUFbFphhNTJsmalJaH0hs4wvWGN/QFxv1wYeaJiRqE02siYnGkPTBaGgfTHipiQjGFwMhtmkbQs2ybZUtZFl3IYssC2yB8WGZnzszzOz8cRH3e9mdM/fcOffc75577g/D8zz08Myhdt0CrfurdfUBMD9MTBiVoYaKV19o4/l4N8/Huw0L5wuryBdWd8CsnQM7PZvB1fGTAIBzh/ULe7l9e88BABjaRtBEhZnCe633AYC9cS9L2waqMMUAL7fPLTuooSQHhGpqEaqpddkUOmDaGx5TzfPKnhYan17KIl9YVb23GRuYWDJhR98W2Ot9rSphfaCKeO75OAsA+OXTZt3K5hcfOWXXjoHVEiob8tU7Pk35XoDmEBAQ9XHgPB5MZBZdM+B6XyvzwTdZy/qDt27Z+r4qCEZ9HPF7J5FGLJnA1NxDRH0coj4OU3MPEUsmsJkviDK5jhl09923Y79tqBgQ9XGILRfE3m9ciwAAZquThKJcXlhZEXViywXTNjzVUG+5AXYZQMQArZ6UN/xOIg0AOHowpHKIXNeCE6iBNUPbowdDJZUzUydtZ7He6hViMfTKqTpiSHw7OKPUYWp9XvFhvaIOnMcD5lFxhrj0XoSvD1Sh9xOSIf0fRhCbXsbn3/9DyI94PDzW
Line truncated
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
// Striding Hero skin pack
|
||||
// - https://www.minecraft.net/content/dam/games/minecraft/software/striding-hero-skinpack.zip
|
||||
// - https://minecraft.wiki/w/Striding_Hero_(skin_pack)
|
||||
Skin {
|
||||
texture_key: Arc::from("721c05483a435d4362047ccb62e075ef5f001aa63a7e0e2afe03e60759bab91d"),
|
||||
name: Some(Arc::from("Snowfeather")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGhUExURQAAAPLR2+60xPDP2eOpueyywu/O2OCmtuuxweiuvuSquumvv+etvfXU3vPS3OqwwOvK1NqgsOGnt++1xfnY4iMjI+KouHRAcWU5Y6pdpp9ZnPHQ2vO5yeasvO7N1+zL1ejH0dacrNuhsffW4KV2ZvjX4fzb5e2zw96ktPva5O3M1v/g6tyisv7d5/bV3/TT3eWru8qTiv3c5t+ltT05OURBQd2js/rZ41VOVDs3O+nI0u7u7oVZxv3u//Xh9Pv7+6p84tOgjygkJsyalebFz/G3x9mfr/C2xoxKe5pZiRAQEBEREQ8PDzM3cICDtXt+r9Kjlbtqp8Z7tQUFBSAjURMTEwMDA3J1oA4ODhcXFxQUFA0NDUFDbWhrkf/e6P/f6T0LMGMcUWRnkzwqIzUlH1E5MFg+NAEBAQICAk9RegQEAwMDAgICAQUFBAEBAHNhVksxIUguHnRiV0ctHXVjWEwyInJgVUowIEkvH040JHZkWQYGBeXXie3elygoKD0cBxMLR4kAAEkkCylt23+u/xxeyWs7G4IAAHJCIsGgiQAAANjyOZsAAACLdFJOU////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////wCFwxXvAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFx0lEQVRYR7VWj3/aRBSnKaXJJYClmHXFbFXkR+tkyejcbzfEOZg2KTiNP8gGY7Sz0LXW/XLq2s45NX+1791dAgTabvt8/HK53F3ufe+9F/LeC7kcE8JkeCoyPSVKRCZiWOHLR8IniEoxJR55ZyZBZgAkzJePhE8QmSURQpJEmnkXMKPyZR/Hjs3NwXWMT330NYgSyiEyAsKXfRw/Pj+PjU99+ARyhEhJJSVzDd7jy0fCJ9BSM+ET6smFMCN4cyfGUsr7Ex9MpLUPKUZ8cBB8ghPw/ibDaVFSUD7++hpkZgnJhnPxdHYqo04lMtORKInmhaykLi4pH52aBP9zx+FzNjo+8C5CH8skHzsRF1OiUpiV45l4WlSiefV0VNfSapokXXdunu5kz+lwfo7eKEKJ5GxeSqmTsmGc0QxDiWcVeKOCcvqUlpGyYswjkF33jOa61LQhgniS5FNFNVZcPmtMn102ThE9KuSJeFoTzyjxT8657vkLuLEI1zQOAGyFIZS7mCcXJXVy+dJlQ798aVkSdSIQEo6oyoJE8mn3/JVPvf06uw2sAEE4NUuWFLkQv3qtVLp21Tg3pU0QoheiipaVphbT7oVPr3zGN3MMrYQSk0SLRrRM3iiXSmVj9vPrJEVyeiFcUPTFREaA/QH54ZUQ+DqhpMOkIAFB6ZycVb6IKvmwIMRU7eSNac/sA0H/SJVKtXLzy69u0hWArguqJuekFQ6+7JqWZa0C+JSCElRr9crN0Nc+gawLgpqTcyu3KHwCkLdWDWMMQb1W/ebbkE9wXRcMI6cmRgjsAwjQhO++/+FHugIQdQOgjphgI0HQhGq9UqvUK/VqvVqDUc2yTLPQKJXLpYZkOY6lk4hSyCdFExyAl+VAM02Hy7shEAYxEGZENXhu3jYbpVLDEk3bcUxhKRa/c2fBdOB42hzbgQdcHghq7OwqksAYCCxLsRsNy4yYtm2bprIYv6MvWKg/HI8EtjVAwM6u1pqtZgt1oMeo9t2GKYPKNvIpi2kdlh3bpAS4w7a4PJqAJzdb7fa9drNVrVtUCAlgq4lWk5hU1DwCBxso1ifAk5utzlr73r12p9NsgTxstO420BSEFsvKRaBF76H+tHf6Jhir8Gahwxs2tNsygYC6HDYXk1oRVQd/Ajc8hA78yOWRAKTW7zMS6FFXJPiJSiNu4HsDGdQC5dEPAyZsdHsbm5sbvR7cNzYfbDIfgAUmHVEOELbhX4ltBf+d2HF5N7S1/fNOaPuX9Yfbjx5vPXm6A1I+ATrOgR5/hlEGDcvlVWigbZnLu6FHW48fPd15+Ov21s72k2fPnlDnowXUBaA7uhQWUR5NpO5aHSDoPuht9noPer91u5sb3W6X6rey8vsfqDGqyzsQRhVAtEwHXL6fWAbhxQM+PRRjCbx4wKeHYiwBxAPXhXjAp4diLIFI47f69iYUnmP/nBJ48QDH4+AR1Ghfp7172wWG5y4TovFgd4GOx4AR1DhBjVMo7t4elE107EI82NdfsPEoxhOoe3++3IN0yqDsp/VdPh6BR9Ci9xYnWNp7+dee9xLIvlR8dQQBE0fwkYwmQGYGaH9n5X/+pcNx8Jx4MPaT2iHynGD9Pr2Nwwv3xqtD5EfjAV/vY3eXvgH8xFgkoCGhHw94t05nb4GjfXAE/h+CYL2AwQnDEJ8OYSxBsF7A4IaBkE+HMJ4gUC9gNH8jgmC9gOnktU3AXI01A1w04bO6AL0AaXGgLvDACOr8I4I7E/Mp6qwuwNQwXBd4YAT9r5HWCSzZ469ao3UBZsNAXeCBEQx8jUxwDbJ1u7221myxtI4EmCv7OdHDiA+YIBDAoANgBJjuhusCD+MJOl690OmA6piwqScG6wIPIwTBeoHVBWj/cF3ggREMxINgvQBHY77F08f7IBgPgnMWB1gQoNGANe+6RTWAzo8HwXqBJWXoAnWBhxEfBOuFg+oCDyMEwXqBno7SgbqAwXX/A/VxkDAomWhdAAAAAElFTkSuQmCC"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("b914cf5106aaa82409fdd9213fbdb1479b4d65aecc5d5e22b1f25e5744c4c4f7"),
|
||||
name: Some(Arc::from("Stray")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,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"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("5eb077c54ecfc7e760c36add887b68859d7a3160d331580ff859f7353d959151"),
|
||||
name: Some(Arc::from("Strider")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAHmUExURQAAAFRnlFJlkltypFNmk1FkkTJBYDFAXy8+XT06STw5SDA/XiUwRzckGuFTAsc9AOG/jTYjGVpxo1lwolVolSQvRiMuRd9RAMk/At+9izkmHEpjky49XCYxSDUiGFxzpUhhkTs4RyItRLZb2FZpli08WztPdj1ReDxQdzlNdDpOdUlikldql95QAOC+jMU7ADglG+BSATdIakdgkDZHaTRFZ927id68ij5SeRgYGN1PAMg+Ae/v7zVGaDNEZjhJa+iY8X2s/X+u//Hx8QEBATonHRxeyUZfj0tklLdc2eeX8Do3Rk9YWzhMcxERERQUFBYWFmZ5oGd6oRMTExUVFQICAgMDA2h7ojNCYVhvoWR3nlduoEVejmV4n1BjkFlsl11wm1tumUxllTlKbDMotTIntCQceyMbeiggjCcfiyYeimdnZyoijlhrljQptlZWVjEmsyYefWZmZiUdiVVVVSUdfCEZeFdXVycfflRUVD9TelptmDAlsiIaeSsrKy4uLi0tLVNTUzAwMDUqt1JSUi8vLywsLAICAQQEAwMDAgUFBAEBAHNhVksxIUguHnRiV0ctHXVjWEwyInJgVUowIEkvH00zI040JHZkWQYGBRISElxvmmRkZCkhjSAYd1hYWHdlWgAAAAn2rnQAAACidFJOU///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AEVMlGgAAAAJcEhZcwAACxMAAAsTAQCanBgAAAcoSURBVFhH7Vb9W9vWFZbkq9iJzRqMbUKVEIXS62tpUFcSDe2q0K20pt06MIHQZg0JmyXZ3mabYoUwwuzSJf7ArmcH0q91q//THXnXIsF0rM+eZ9sPe3ls6V7OeX3Ovfe89zBdCpbzIMQj7pzXd/6C1x+g0y6GfvTCxaHh4WE6dOEScMERlgux3LlwOOL1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"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("b271a744ef479018927575952621b110b9c11f62730a95729af7e8591cf8dbf6"),
|
||||
name: Some(Arc::from("Villager 1")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADDUExURQAAADlIXj9QaHyczI9vYIhkV4Gj02xNOyMjI0xma0NaXnSTv3CXnmmNk2yJsubw8pqgoKevr/L9/83X2DMkEVBTVDg6Ov///wDTFYdkVIpoX5BuXCQlJlg3LmN+o19/hFFHRWiLkaqnr2RgZnKZoHunr0Q8Op674mBlZoCttcHKzDZcEitMD2OoI4HbLkh9GG1xcnHAKFGJG7O7vL/IyUJUbUGdrTZEWcy/euXXier194Sm2C47TEhLTCYxPys3RwAAAMIErkMAAABBdFJOU/////////////////////////////////////////////////////////////////////////////////////8AMFXsxwAAAAlwSFlzAAALEwAACxMBAJqcGAAAA/JJREFUWEftlntX2zgQxROpZBcCUSxvabvZJQvIxDYJkAanLSmpv/+n2ntH8iMPDpTTPWf/4Dq2rMf8PDNyLHXKoK5SWqtu953XQWh+VjVA6Z7Wuqfe/SZ6DYD2BPwO7QIOD4+OcB6Gaq0aoHWfCP0U4Pj45IS/UK3V9qCP40kPnlILQA8Grwd0db+vB4Puc0k0G0UboBQC6CrMoCg078gMeR02AM3px2Eiq5SNjFRCI2+R/53EHbfmotMb9HoIPzbWRkNrTYTJxHTi2u8PBhqAoxMZaUw0NFZuT46kEHUQN4bBPqLjQEQaSDRpsa8A6Odw9OO6CdAcBsAfIgK02FOYmLJ8f1qW6Pfj0e9bKgFAxdZ++Hhw8PGDxAB7/1b1B7p8b81piX4/Hv2+pVID+PTnaDT6JACI7iMGAE6N/WsTIC2VGAKGMQd/QxFKaalVlqcYzX4OZ79vqdRR6uwM0x+bIRwYDU2ECuYQf2x0nCkVxqGfBfql2khepDH0DyQt0PnFJXRx7oJCc5lA2y9ZA7i6qgEXlxx2eeHORQ3AueRg62PzCwGbIdD+ZSEk43GSTFITxyn7kwxnbNPRKDVRljg81Dm2BWW8xyUL9gCQAECepxPHITRIbZrmqcCAuJ6yyDKXIQRAcXEtAOrjBB7AAOGieu6yDEDvEEyns9mUAHrCghYuCfZlx43x3MoDAkBxTgAYiHM6u7m5hQ94KqrA0HwDgBoByIEA6GF2Z1BFDSbXs5v57TVR9J4iqE4scgAAQgBgwjupJneWPKldz+bzz8wCUgdjZoFlsGcOJInWA+CPIFIBCGF6O5/fTuEaJwCZlDRmDWAycQ4R2Dy3SNuEEBjCg7sASKafEYE8WCaZnDZgcb9Y3BtMmzW4W9wziYlSRaGUjwEEZABi9pgflkmTA76BbRU0XX75+u3LUuEONWkpHh4erMXpS/sQ7AMA/4OqXC5Xy5X+/u3r99VqqZfQSvT4mMovlGmwLztXW1oXxRoqih84i3WorNdYLvA34uHLYN8sLK/VG+ANQL0BfgIgHwL/TeBN2C78b0PYXmz5RdxeVCs9DWgt9/yYbi/rlf5DwGtDSNI4NlikZN/gJXsCucFtGFZrBzBJ85yAYM9lyq+J3FVgXQ3Dau3xIIcH2DLAUtZ6rGNc2Pyi3KxIlfZ54EPgxiGs91hRw/bgBQB4ICFg3yIA7igkDpx0KgyrtceDGAA+TFLAzBHiF3aUYVitvQArSaQPASCrukCeDwFbi9wiBuwaIDgh1vSGk/gCD5q9wj23DgvuRhg8ncH1eQDfwLa2vwPb9f2A1n6htSfgtuAxDKu1AwjbhFrcD/CoyjAsqCz/BZr8DQ1gI8/rAAAAAElFTkSuQmCC"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("748923629fed7c6ec9462016b4480fa3cff8c16e82ee6fe26d4b707f4de10060"),
|
||||
name: Some(Arc::from("Villager 2")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADYUExURQAAAGE3OWw9QNR4e9WyndGnltt8gbiUfmxsbGA4elIxasdwdIpRsoBOprhobPLl66CZnbCmrNjM0//x+TMkEVNNVjk3O////2YA0/Tr/+3e99Cok9GsntexmikkKaeAdqlfY3RfhFFHRYBokaqnr2RgZo1yoJp7r0Q8OtiE1u3m8tPN2MjBzJ+Atfry/zkQA2wsGXUyHnBtcl0iELe3t5mZmU0YBxYQBbizvMS/yWFVQ2VYRqFeU1JIOYuVx52o4PLq98mwjL6nhEY9MUtITEI5LToyKAAAAMtUDGQAAABIdFJOU///////////////////////////////////////////////////////////////////////////////////////////////AJzs8mAAAAAJcEhZcwAACxMAAAsTAQCanBgAAAPvSURBVFhH7ZZtd9o4EIWN1dBNSDAO2/Rt2YY6lgIEU5psS4C2IS3x//9He+9Ilo0Jp9mc9pz9kGtsWdLM45FkrAlyp0YYKhU2Gs+s9lzzT+UBoWoqpZrhs+eixwDoT8Af0DZgf//gAOe+q3p5gFItItQuwOHh0RF/rupVjaCFY2cEu1QBMIL24wEN1Wqpdrvxs0mMNooqIAwxgEaIFRS55i1FHV47JUBx+XHEx3EY4iIV18hbzP/WxB1W1iJotptNDP846nbjTrcbxVhMLCeurVa7rQA4OBLLKIo7UVdujw6kEAUYN8zgHzNwIGIFJJqU+BcA9NMc/bhuAhTNAPhTRIASfwoLk+cvTvIc/dYe/balEADUcbf78tXe3quXMgb427eq1Vb5i9dvTnL0W3v025ZCJeDtX71e760AIIaPMQBw8ub135sAaSnEIcCMc/AOilFKi1een8Ca/TRnv20pFITh6SmW/zjqIIBeJ4pRwRrij42O0zB0duhngX6plpIXqQ+9h6QFSs5S6CzRTq45N1D9JSsB5+cecJbSLD3TiagEaG32ah+bXwjYHAL9HzYEA2czSIeji9SYvjFjGI2ytNdD09hoPFRrtpXSJjFJ4vzzoN/XBoBsNEoHfQ0CHdIsTQGkozbj8YQFHRETh8HC+SMCVIxJswsANGBaJ3o8TocMyGryYTpNBCDOaKkB4IUIAODDDSYOhQCI1pMpj2nCDgHYKPy8BLoEYAh8CIP+OASPjzVwnkw/YBCo4gAHJjidPyJgaBgC5oBzCLH+MWNArF1eTS8nV/98gifDk0YG4/xlCFyFDKswAFsYmkA7hCS5vIL7ZzozfvTZ0/nnwWCgNVYRgAzTNiAExh4AxOWnzzYsmUDGTzn/PJhdz2bXQyxbNsTd7JqTaObzxWI+t150lhdBJg8Ve3H+ecA3sKoFXZdfvn77spzjDjVpWdzc3GQZTltmN87fAfA/KMrlcrVc3X7/9vX7arW8XUKr1e3tavXjRyo/V6bOPw/Oa1qsF+v13d1isb5br/Hwtbtiu8DfiIctnX+5sTxWT4AnAPUE+A+A6reApWv+vw6hvtnyi1jfVAvtBlS2e35H69t6od8IeOwQTDq6GGKTAgNudrPj1sCcgHfOzGsLMAAgA0C2PCQM1ZyAIGfmdR8ACQpt4WATBp8TPAiAIQCALbLY6wlwOQG3ZWfmtXMIAEgI1ZyAcTgzrx0RSLRUv5oTcIt3Zl73RHAhQ2DyJUFUcoIHAZjsZFhHZA3QZk5AOTOvLUCZK1wzdZhxEmUmeaB0Zl5bAL6BVdW/A/X6/YBKvlDNCXhxZl5bAJcmeFVzApbOzCnP/wXGOWDitX7aLAAAAABJRU5ErkJggg=="
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("3d996abc69ea70a20442855e429bf44b45111f9818d0f8c46272e12d12bec218"),
|
||||
name: Some(Arc::from("Wither Skeleton")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAABOUExURQAAABsbGxUVFSkpKTQ0NB8fHzxBQUtNTVFTUw4ODjM0NDIzMzQ1NTIyMjAxMTEyMjU2Ni8wMC4vLxwcHCgoKEQSEzcFBl8tLlUjJAAAAGwkFmAAAAAadFJOU/////////////////////////////////8AFCIA2gAAAAlwSFlzAAALEwAACxMBAJqcGAAABNpJREFUWEfFl+liqzYQhUELclhMbhNs8v4v2u/MCLw3bfKjEyMkMXNm1ZLmq1LbhjbGlpdTnf6WLgAxpiQEH/53AIknnh8DIJxTjunnABH5X1oQW7z4TQwy8psF9fU93bjgaVASt2R8T002Ug4VATMixjojwMr3kgCAtSuHQycE65WuA8Tr4t8AZMoPGNONDRq7UaJc+V5S05rW0qWmSTxmgHzZqPK9pEZrILjDTjF6FF3+22A2sKaS0VtUyk1zKLkkSwWe8ap8L6kJb6FNAY9zI+IdUsvkRpXvJTVvoeu6g5KgbNA5HOTD20aV7yVZIcnc6+Lph3EaIN8gYqrTX2Li78aqpwBDyPqbUt+37XHYASTM7xHAxS8fZpMPU+ynFN/7vZiM6UqR6ClALw+macJ8rdBdwPWHWwArvNTGHHnoCioqLSExZIQtMWTKog08/FLLxrODNJKCldoTTpvG4zwizLIKeDCP058x5uE48F2QlmUW3gUgIccE2xEPfXRqCRS0YL8skBM0+q5dD+tA2kvcAFT5zCdMjWRw0qqOIAx/yYahS8TDvmvPUXhhrPJUImiF9ROKXPYY4CLlLOcsBqkjBinYEpMObLsCgLF0CkwXC0HO/XGahjzmjyFPcz9QU0NMmMVCIYhoV4Zpq7yWM4qks7M8xGHopz6kT3x676exn+c54tcIn/D5YTKMVV4Aij/xyrEo7TMRP059/pynkffQH8c2g2MhxF8SpDjs1aktjRBqG4xd7mLbQ9M4f+SBFNKdpjm0PanVlsnPAGirPABmABjyreQ49e/z8D595gH1f47AHdswD71iYAAEUzBV3nckHpwLRQ+Lu5QYPsgVydFIB57iTEZUyBCBvwTRFuzlKUpe7FBEo+DiHRpxgK6gqAG5sS8wW0wLZKMfkAOcTv83wG9cOK9naD2vkHpG/u2qv009UrMu4lokf4dQu+p7+5SaBdnzuqwaOIIajdTXwCc3cGNmIA5RcxLCejKAha+3AGK1X22k7LyAcQH4Oknwy4K4A7g9AsBBnzJZCF49e9BrFqzPLAKmVCPXZj+bs+ECs33dyAC2KjjpK/hnGyOwyF819aUPMBlzJQfADSd9hdn6ZquMUgcdK4qM8RHgQie3wvpAScJl5Z5935qN7gAMonq0eM+M8tczegAQuQq1PGaUTTylpwAPdGP0LRnA/WKy84CnDv1QZeeqwxtygLvl/ABg8pfxNT0FcPHLPYCRfv8AcOfCA4Ab8BzAVrFq3ZuzHU92XLHV0+hGwJ6vl3bVfTvfyAC2PYFm1SmhE1s7su4LvHTo++vqWN/IXFCVg6KVt+qk1zGmS4ceDSVosxxt1VCxSuzsACeNlpOWjx2AnHMS5gzEH10K/FjkSEhSBQiiLC1wPIjYoGoRQJsLF4JSpI/jiuuBjk3ONQEQINRChqI1sxiA8mAtG0WJHZeFtiuKpAXf7gN2KmJZ1oYgC1b2JKndAKxYgTnF3NndRHFUPpQAcHR/0hUgw6gtzdUjUwHqjkCrw5Gj0HOgayKy/CyEOinFhTrb1K4AKgHQyWW7MijzHWI6XAHQucoH53IAdW4BIHMZaZnLba8CKAbq7VcbnPXOA0ChbHHayl99lbBa/onAn/axEut7p+vrQlEU7NaKMbo6hnS3Hr6+/gadlgnNKz1F1wAAAABJRU5ErkJggg=="
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
// The Garden Awakens skin pack
|
||||
// - https://www.minecraft.net/content/dam/minecraftnet/games/minecraft/software/TheGardenAwakens_JavaSkin_CreakingSkin_B.zip
|
||||
// - https://minecraft.wiki/w/The_Garden_Awakens_(skin_pack)
|
||||
Skin {
|
||||
texture_key: Arc::from("6f8fc677cdcd4c6eed67d90c08d23162abc3a3a85357c7636fdf80d874aa857f"),
|
||||
name: Some(Arc::from("Pale Lumberjack")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,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"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("9a0af2b1fd9659480d43132db95cd7d459d1a66480fe42150e132d03b9731573"),
|
||||
name: Some(Arc::from("Creaking")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,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"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
// Chase the Skies skin pack
|
||||
// - https://www.minecraft.net/content/dam/minecraftnet/games/minecraft/software/ChaseTheSkies_JavaSkins.zip
|
||||
// - https://minecraft.wiki/w/Chase_the_Skies_(skin_pack)
|
||||
Skin {
|
||||
texture_key: Arc::from("8409954698b6c7741460fdd85d6ec6a5e0a9ad04ade7e2c72c913f02936a607d"),
|
||||
name: Some(Arc::from("Ghast Pilot")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,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"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
},
|
||||
Skin {
|
||||
texture_key: Arc::from("e12d98dab548e92cad7ac80f92d8fefbb9ca7a1af94aa4f428daf6ef723aa8e0"),
|
||||
name: Some(Arc::from("Ghast Swimmer")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,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"
|
||||
).unwrap()),
|
||||
source: SkinSource::Default,
|
||||
is_equipped: false,
|
||||
}]
|
||||
});
|
||||
@@ -619,6 +619,12 @@ pub async fn launch_minecraft(
|
||||
.into_iter(),
|
||||
);
|
||||
|
||||
// The java launcher requires access to java.lang.reflect in order to force access in to
|
||||
// whatever module the main class is in
|
||||
if java_version.parsed_version >= 9 {
|
||||
command.arg("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED");
|
||||
}
|
||||
|
||||
// The java launcher code requires internal JDK code in Java 25+ in order to support JEP 512
|
||||
if java_version.parsed_version >= 25 {
|
||||
command.arg("--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['custom/library'],
|
||||
ignorePatterns: ['**/*.scss', '**/*.svg', 'node_modules/', 'dist/'],
|
||||
ignorePatterns: ['**/*.scss', '**/*.svg', 'node_modules/', 'dist/', '**/*.gltf'],
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ import _BellRingIcon from './icons/bell-ring.svg?component'
|
||||
import _BellIcon from './icons/bell.svg?component'
|
||||
import _BlocksIcon from './icons/blocks.svg?component'
|
||||
import _BoldIcon from './icons/bold.svg?component'
|
||||
import _BookOpenIcon from './icons/book-open.svg?component'
|
||||
import _BookTextIcon from './icons/book-text.svg?component'
|
||||
import _BookIcon from './icons/book.svg?component'
|
||||
import _BookmarkIcon from './icons/bookmark.svg?component'
|
||||
@@ -19,6 +20,7 @@ import _BotIcon from './icons/bot.svg?component'
|
||||
import _BoxImportIcon from './icons/box-import.svg?component'
|
||||
import _BoxIcon from './icons/box.svg?component'
|
||||
import _BracesIcon from './icons/braces.svg?component'
|
||||
import _BrushCleaningIcon from './icons/brush-cleaning.svg?component'
|
||||
import _CalendarIcon from './icons/calendar.svg?component'
|
||||
import _CardIcon from './icons/card.svg?component'
|
||||
import _ChangeSkinIcon from './icons/change-skin.svg?component'
|
||||
@@ -86,6 +88,7 @@ import _InfoIcon from './icons/info.svg?component'
|
||||
import _IssuesIcon from './icons/issues.svg?component'
|
||||
import _ItalicIcon from './icons/italic.svg?component'
|
||||
import _KeyIcon from './icons/key.svg?component'
|
||||
import _KeyboardIcon from './icons/keyboard.svg?component'
|
||||
import _LanguagesIcon from './icons/languages.svg?component'
|
||||
import _LeftArrowIcon from './icons/left-arrow.svg?component'
|
||||
import _LibraryIcon from './icons/library.svg?component'
|
||||
@@ -166,6 +169,7 @@ import _TextQuoteIcon from './icons/text-quote.svg?component'
|
||||
import _TimerIcon from './icons/timer.svg?component'
|
||||
import _TransferIcon from './icons/transfer.svg?component'
|
||||
import _TrashIcon from './icons/trash.svg?component'
|
||||
import _TriangleAlertIcon from './icons/triangle-alert.svg?component'
|
||||
import _UnderlineIcon from './icons/underline.svg?component'
|
||||
import _UndoIcon from './icons/undo.svg?component'
|
||||
import _UnknownDonationIcon from './icons/unknown-donation.svg?component'
|
||||
@@ -199,6 +203,7 @@ export const BellRingIcon = _BellRingIcon
|
||||
export const BellIcon = _BellIcon
|
||||
export const BlocksIcon = _BlocksIcon
|
||||
export const BoldIcon = _BoldIcon
|
||||
export const BookOpenIcon = _BookOpenIcon
|
||||
export const BookTextIcon = _BookTextIcon
|
||||
export const BookIcon = _BookIcon
|
||||
export const BookmarkIcon = _BookmarkIcon
|
||||
@@ -206,6 +211,7 @@ export const BotIcon = _BotIcon
|
||||
export const BoxImportIcon = _BoxImportIcon
|
||||
export const BoxIcon = _BoxIcon
|
||||
export const BracesIcon = _BracesIcon
|
||||
export const BrushCleaningIcon = _BrushCleaningIcon
|
||||
export const CalendarIcon = _CalendarIcon
|
||||
export const CardIcon = _CardIcon
|
||||
export const ChangeSkinIcon = _ChangeSkinIcon
|
||||
@@ -273,6 +279,7 @@ export const InfoIcon = _InfoIcon
|
||||
export const IssuesIcon = _IssuesIcon
|
||||
export const ItalicIcon = _ItalicIcon
|
||||
export const KeyIcon = _KeyIcon
|
||||
export const KeyboardIcon = _KeyboardIcon
|
||||
export const LanguagesIcon = _LanguagesIcon
|
||||
export const LeftArrowIcon = _LeftArrowIcon
|
||||
export const LibraryIcon = _LibraryIcon
|
||||
@@ -353,6 +360,7 @@ export const TextQuoteIcon = _TextQuoteIcon
|
||||
export const TimerIcon = _TimerIcon
|
||||
export const TransferIcon = _TransferIcon
|
||||
export const TrashIcon = _TrashIcon
|
||||
export const TriangleAlertIcon = _TriangleAlertIcon
|
||||
export const UnderlineIcon = _UnderlineIcon
|
||||
export const UndoIcon = _UndoIcon
|
||||
export const UnknownDonationIcon = _UnknownDonationIcon
|
||||
|
||||
@@ -9,3 +9,8 @@ declare module '*.webp' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
|
||||
declare module '*?url' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-open-icon lucide-book-open"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>
|
||||
|
After Width: | Height: | Size: 403 B |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-brush-cleaning-icon lucide-brush-cleaning">
|
||||
<path d="m16 22-1-4" />
|
||||
<path
|
||||
d="M19 13.99a1 1 0 0 0 1-1V12a2 2 0 0 0-2-2h-3a1 1 0 0 1-1-1V4a2 2 0 0 0-4 0v5a1 1 0 0 1-1 1H6a2 2 0 0 0-2 2v.99a1 1 0 0 0 1 1" />
|
||||
<path d="M5 14h14l1.973 6.767A1 1 0 0 1 20 22H4a1 1 0 0 1-.973-1.233z" />
|
||||
<path d="m8 22 1-4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 542 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-keyboard-icon lucide-keyboard"><path d="M10 8h.01"/><path d="M12 12h.01"/><path d="M14 8h.01"/><path d="M16 12h.01"/><path d="M18 8h.01"/><path d="M6 8h.01"/><path d="M7 16h10"/><path d="M8 12h.01"/><rect width="20" height="16" x="2" y="4" rx="2"/></svg>
|
||||
|
After Width: | Height: | Size: 456 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
class="lucide lucide-triangle-alert-icon lucide-triangle-alert">
|
||||
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" />
|
||||
<path d="M12 9v4" />
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 403 B |
@@ -83,4 +83,8 @@ export const TwitterIcon = _TwitterIcon
|
||||
export const WindowsIcon = _WindowsIcon
|
||||
export const YouTubeIcon = _YouTubeIcon
|
||||
|
||||
// Skin Models
|
||||
export { default as ClassicPlayerModel } from './models/classic-player.gltf?url'
|
||||
export { default as SlimPlayerModel } from './models/slim-player.gltf?url'
|
||||
|
||||
export * from './generated-icons'
|
||||
@@ -2,6 +2,7 @@
|
||||
title: A New Chapter for Modrinth Servers
|
||||
summary: Modrinth Servers is now fully operated in-house by the Modrinth Team.
|
||||
date: 2025-03-13T00:00:00+00:00
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG']
|
||||
---
|
||||
|
||||
Over the few months, Modrinth has seen incredible interest towards our Servers product, and with significant growth, our vision for what Modrinth Servers can be has evolved alongside it. To continue striving towards our goal of providing the best place to get your own Minecraft multiplayer server, we’ve made the decision to bring our server hosting fully in-house.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: Accelerating Modrinth's Development
|
||||
summary: Our fundraiser and the future of Modrinth!
|
||||
date: 2023-02-01T12:00:00-08:00
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG', '6plzAzU4']
|
||||
---
|
||||
|
||||
**Update: On [April 4, 2024](/news/article/capital-return) we announced that we had returned the remaining $800k in investor capital back to our investors to take a different path. [Read that announcement here](/news/article/capital-return). This article remains here for archival purposes.**
|
||||
|
||||
@@ -4,6 +4,7 @@ short_title: Becoming Sustainable
|
||||
summary: Announcing an update to our monetization program, creator split, and more!
|
||||
short_summary: Announcing 5x creator revenue and updates to the monetization program.
|
||||
date: 2024-09-13T12:00:00-08:00
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG']
|
||||
---
|
||||
|
||||
Just over 3 weeks ago, we [launched](/news/article/introducing-modrinth-refreshed-site-look-new-advertising-system) our new ads powered by [Aditude](https://www.aditude.com/). These ads have allowed us to improve creator revenue drastically and become sustainable. Read on for more info!
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: A Sustainable Path Forward for Modrinth
|
||||
summary: Our capital return and what’s next.
|
||||
date: 2024-04-04T12:00:00-08:00
|
||||
authors: ['MpxzqsyW']
|
||||
---
|
||||
|
||||
Over three years ago, I started Modrinth: a new Minecraft modding platform built on community principles, a fully open-source codebase, and a focus on creators.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
title: Modrinth's Carbon Ads experiment
|
||||
summary: "As a step towards implementing author payouts, we're experimenting with a couple different ad providers to see which one works the best for us."
|
||||
summary: 'Experimenting with a different ad providers to find one which one works for us.'
|
||||
date: 2022-09-08
|
||||
authors: ['6plzAzU4']
|
||||
---
|
||||
|
||||
**Update 10/24:** A month and a half ago we began testing Carbon Ads on Modrinth, and in the end, using Carbon did not work out. After disabling ads with tracking in them, the revenue was about equal to or worse than what we were generating previously with EthicalAds. Effective today, we are switching our ads provider back to EthicalAds for the time being.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
title: Creators can now make money on Modrinth!
|
||||
summary: "Yes, you read the title correctly: Modrinth's creator monetization program, also known as payouts, is now in an open beta phase. Read on for more information!"
|
||||
summary: 'Introducing the Creator Monetization Program allowing creators to earn revenue from their projects.'
|
||||
date: 2022-11-12
|
||||
authors: ['6plzAzU4']
|
||||
---
|
||||
|
||||
Yes, you read the title correctly: Modrinth's Creator Monetization Program, also known as payouts, is now in an open beta phase. All of the money that project owners have earned since August 1st is available to claim **right now**!
|
||||
|
||||
@@ -4,6 +4,7 @@ short_title: The Creator Update
|
||||
summary: December may be over, but we’re not done giving gifts.
|
||||
short_summary: Adding analytics, orgs, collections, and more!
|
||||
date: 2024-01-06T12:00:00-08:00
|
||||
authors: ['6plzAzU4']
|
||||
---
|
||||
|
||||
December may be over, but that doesn’t mean we’re done giving gifts here at Modrinth. Over the past few months, we’ve been cooking up a whole bunch of new features for everyone to enjoy. Now seems like as good of a time as ever to bring you our Creator Update! Buckle up, because this is a big one.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: Creator Updates, July 2025
|
||||
summary: Addressing recent growth and growing pains that have been affecting creators.
|
||||
date: 2025-07-01T21:20:00-07:00
|
||||
authors: ['MpxzqsyW']
|
||||
---
|
||||
|
||||
Hey all,
|
||||
|
||||
@@ -4,6 +4,7 @@ short_title: Modrinth+ and New Ads
|
||||
summary: Learn about this major update to Modrinth.
|
||||
short_summary: Introducing a new ad system, a subscription to remove ads, and a redesign of the website!
|
||||
date: 2024-08-21T12:00:00-08:00
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG']
|
||||
---
|
||||
|
||||
We’ve got a big launch with tons of new stuff today and some important updates about Modrinth. Read on, because we have a lot to cover!
|
||||
|
||||
@@ -3,6 +3,7 @@ title: Correcting Inflated Download Counts due to Rate Limiting Issue
|
||||
short_title: Correcting Inflated Download Counts
|
||||
summary: A rate limiting issue caused inflated download counts in certain countries.
|
||||
date: 2023-11-10T12:00:00-08:00
|
||||
authors: ['6plzAzU4', 'MpxzqsyW']
|
||||
---
|
||||
|
||||
While working on the upcoming analytics update for Modrinth, our team found an issue leading to higher download counts from specific countries. This was caused by an oversight with regards to rate limiting, or in other words, certain files being downloaded over and over again. **Importantly, this did not affect creator payouts**; only our analytics. Approximately 15.4% of Modrinth downloads were found to be over-counted. These duplicates have been identified and are being removed from project download counts and analytics. Read on to learn about the cause of this error and how we fixed it.
|
||||
|
||||