You've already forked AstralRinth
forked from didirus/AstralRinth
Compare commits
97 Commits
AR-0.10.30
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
| 46d30e491a | |||
| 059c0618f1 | |||
| 7ef60fcafe | |||
| ec17e79014 | |||
| e351d674f4 | |||
| f555fa916a | |||
| dbe38cb4e7 | |||
| 2e40e26116 | |||
|
|
ae25a15abd | ||
|
|
0f755b94ce | ||
|
|
bcf46d440b | ||
|
|
526561f2de | ||
|
|
a8caa1afc3 | ||
|
|
98e9a8473d | ||
|
|
936395484e | ||
|
|
0c3e23db96 | ||
|
|
013ba4d86d | ||
|
|
93813c448c | ||
|
|
c20b869e62 | ||
|
|
56c556821b | ||
|
|
44267619b6 | ||
| 10afd673db | |||
|
|
90043fe84d | ||
|
|
a6a98ff63e | ||
|
|
911652133b | ||
|
|
cee1b5f522 | ||
|
|
62f5a23fcb | ||
| 5a10292add | |||
| 3f606a08aa | |||
| 2d5d747202 | |||
|
|
eb595cdc3e | ||
| 7516ff9e47 | |||
|
|
572cd065ed | ||
| df9bbe3ba0 | |||
| 362fd7f32a | |||
|
|
76dc8a0897 | ||
|
|
4723de6269 | ||
|
|
e15fa35bad | ||
|
|
2cc6bc8ce4 | ||
|
|
5d19d31b2c | ||
|
|
c1b95ede07 | ||
|
|
058185c7fd | ||
|
|
6fb125cf0f | ||
|
|
a945e9b005 | ||
|
|
b943638afb | ||
|
|
207dc0e2bb | ||
|
|
359fbd4738 | ||
| adf831dab9 | |||
| efeac22d14 | |||
| 591d98a9eb | |||
| 77472d9a09 | |||
| 789d666515 | |||
| d917bff6ef | |||
| 4e69cd8bde | |||
| b71e4cc6f9 | |||
| a56ab6adb9 | |||
| f1b67c9584 | |||
| 3d32640b83 | |||
| 6dfb599e14 | |||
| 332a543f66 | |||
| 1ef96c447e | |||
| 1ec92b5f97 | |||
| f0a4532051 | |||
|
|
f7700acce4 | ||
|
|
87a3e2d022 | ||
|
|
5d17663040 | ||
|
|
cff3c72f94 | ||
|
|
fadf475f06 | ||
|
|
7228499737 | ||
|
|
bca467a634 | ||
| 14f6450cf4 | |||
| 14bf06e4bd | |||
|
|
cb72d2ac80 | ||
|
|
3c79607d1f | ||
| 97bd18c7b3 | |||
| 8af0288274 | |||
| 167072de0c | |||
| 2df37be9a7 | |||
| 34d85a03b2 | |||
| 17cf5e3132 | |||
|
|
36ad1f16e4 | ||
|
|
5d4f334505 | ||
|
|
1fdb5ba748 | ||
| c5e67a5c6f | |||
| e2e21c1496 | |||
| 6da942ccbb | |||
|
|
26df6f51ef | ||
|
|
6caf794ae1 | ||
|
|
2692953e31 | ||
|
|
242fd713ab | ||
|
|
7a12c4d5e2 | ||
| 0ab4dec62d | |||
|
|
f256ef43c0 | ||
|
|
e0cde2d6ff | ||
|
|
e4e77dc0d2 | ||
|
|
8ba6467f21 | ||
|
|
088cb54317 |
1
.dockerignore
Symbolic link
1
.dockerignore
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
.gitignore
|
||||||
73
.github/workflows/astralrinth-build.yml
vendored
73
.github/workflows/astralrinth-build.yml
vendored
@@ -16,6 +16,7 @@ on:
|
|||||||
- 'packages/assets/**'
|
- 'packages/assets/**'
|
||||||
- 'packages/ui/**'
|
- 'packages/ui/**'
|
||||||
- 'packages/utils/**'
|
- 'packages/utils/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -24,12 +25,12 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# platform: [macos-latest, windows-latest, ubuntu-latest]
|
# platform: [macos-latest, windows-latest, ubuntu-latest]
|
||||||
platform: [ubuntu-latest]
|
platform: [windows-latest, ubuntu-latest]
|
||||||
include:
|
include:
|
||||||
# - platform: macos-latest
|
# - platform: macos-latest
|
||||||
# artifact-target-name: universal-apple-darwin
|
# artifact-target-name: universal-apple-darwin
|
||||||
# - platform: windows-latest
|
- platform: windows-latest
|
||||||
# artifact-target-name: x86_64-pc-windows-msvc
|
artifact-target-name: x86_64-pc-windows-msvc
|
||||||
- platform: ubuntu-latest
|
- platform: ubuntu-latest
|
||||||
artifact-target-name: x86_64-unknown-linux-gnu
|
artifact-target-name: x86_64-unknown-linux-gnu
|
||||||
|
|
||||||
@@ -41,6 +42,35 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- name: 🔍 Validate Git config does not introduce CRLF
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "🔍 Checking Git config for CRLF settings..."
|
||||||
|
|
||||||
|
autocrlf=$(git config --get core.autocrlf || echo "unset")
|
||||||
|
eol_setting=$(git config --get core.eol || echo "unset")
|
||||||
|
|
||||||
|
echo "core.autocrlf = $autocrlf"
|
||||||
|
echo "core.eol = $eol_setting"
|
||||||
|
|
||||||
|
if [ "$autocrlf" = "true" ]; then
|
||||||
|
echo "⚠️ WARNING: core.autocrlf is set to 'true'. Consider setting it to 'input' or 'false'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$eol_setting" = "crlf" ]; then
|
||||||
|
echo "⚠️ WARNING: core.eol is set to 'crlf'. Consider unsetting it or setting to 'lf'."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: 🔍 Check migration files line endings (LF only)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo "🔍 Scanning migration SQL files for CR characters (\\r)..."
|
||||||
|
if grep -Iq $'\r' packages/app-lib/migrations/*.sql; then
|
||||||
|
echo "❌ ERROR: Some migration files contain CR (\\r) characters — expected only LF line endings."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ All migration files use LF line endings"
|
||||||
|
|
||||||
- name: 🧰 Setup Rust toolchain
|
- name: 🧰 Setup Rust toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||||
with:
|
with:
|
||||||
@@ -73,11 +103,11 @@ jobs:
|
|||||||
- name: 🧰 Install dependencies
|
- name: 🧰 Install dependencies
|
||||||
run: pnpm install
|
run: pnpm install
|
||||||
|
|
||||||
# - name: ✍️ Set up Windows code signing (jsign)
|
- name: ✍️ Set up Windows code signing (jsign)
|
||||||
# if: matrix.platform == 'windows-latest' && env.SIGN_WINDOWS_BINARIES == 'true'
|
if: matrix.platform == 'windows-latest' && env.SIGN_WINDOWS_BINARIES == 'true'
|
||||||
# shell: bash
|
shell: bash
|
||||||
# run: |
|
run: |
|
||||||
# choco install jsign --ignore-dependencies
|
choco install jsign --ignore-dependencies
|
||||||
|
|
||||||
- name: 🗑️ Clean up cached bundles
|
- name: 🗑️ Clean up cached bundles
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -99,20 +129,25 @@ jobs:
|
|||||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
# - name: 🔨 Build Windows app
|
- name: 🔨 Build Windows app
|
||||||
# if: matrix.platform == 'windows-latest'
|
if: matrix.platform == 'windows-latest'
|
||||||
# shell: pwsh
|
shell: pwsh
|
||||||
# run: |
|
run: |
|
||||||
# $env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
|
$env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
|
||||||
# pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis'
|
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis'
|
||||||
# env:
|
env:
|
||||||
# TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||||
# TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||||
|
|
||||||
- name: 📤 Upload app bundles
|
- name: 📤 Upload app bundles
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: App bundle (${{ matrix.artifact-target-name }})
|
name: App bundle (${{ matrix.artifact-target-name }})
|
||||||
path: |
|
path: |
|
||||||
target/release/bundle/**
|
target/release/bundle/appimage/AstralRinth App_*.AppImage*
|
||||||
target/*/release/bundle/**
|
target/release/bundle/deb/AstralRinth App_*.deb*
|
||||||
|
target/release/bundle/rpm/AstralRinth App-*.rpm*
|
||||||
|
target/universal-apple-darwin/release/bundle/macos/AstralRinth App.app.tar.gz*
|
||||||
|
target/universal-apple-darwin/release/bundle/dmg/AstralRinth App_*.dmg*
|
||||||
|
target/release/bundle/nsis/AstralRinth App_*-setup.exe*
|
||||||
|
target/release/bundle/nsis/AstralRinth App_*-setup.nsis.zip*
|
||||||
|
|||||||
482
Cargo.lock
generated
482
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -67,7 +67,12 @@ heck = "0.5.0"
|
|||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
hickory-resolver = "0.25.2"
|
hickory-resolver = "0.25.2"
|
||||||
hmac = "0.12.1"
|
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"
|
hyper-util = "0.1.14"
|
||||||
iana-time-zone = "0.1.63"
|
iana-time-zone = "0.1.63"
|
||||||
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
image = { version = "0.25.6", default-features = false, features = ["rayon"] }
|
||||||
|
|||||||
159
README.md
159
README.md
@@ -1,76 +1,123 @@
|
|||||||
# Navigation in this README
|
# 📘 Navigation
|
||||||
- [Install instructions](#install-instructions)
|
|
||||||
- [Features](#features)
|
- [🔧 Install Instructions](#install-instructions)
|
||||||
- [Getting started](#getting-started)
|
- [✨ Features](#features)
|
||||||
- [Disclaimer](#disclaimer)
|
- [🚀 Getting Started](#getting-started)
|
||||||
- [Donate](#support-our-project-crypto-wallets)
|
- [⚠️ Disclaimer](#disclaimer)
|
||||||
|
- [💰 Donate](#support-our-project-crypto-wallets)
|
||||||
|
|
||||||
|
## Other languages
|
||||||
|
> [Русский](readme/ru_ru/README.md)
|
||||||
|
|
||||||
|
## Support channel
|
||||||
|
> [Telegram](https://me.astralium.su/ref/telegram_channel)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# About Project
|
# About Project
|
||||||
|
|
||||||
## AstralRinth • Empowering Your Minecraft Adventure
|
## **AstralRinth • Empowering Your Minecraft Adventure**
|
||||||
Welcome to AR • Fork of Modrinth, the ultimate game launcher designed to enhance your Minecraft experience through the Modrinth platform and their API. Whether you're a graphical interface enthusiast, or a developer integrating Modrinth projects, Theseus core is your gateway to a new level of Minecraft gaming.
|
|
||||||
|
|
||||||
## About Software
|
Welcome to **AstralRinth (AR)** — a powerful fork of Modrinth, reimagined to enhance your Minecraft journey. Whether you're a GUI enthusiast or a developer building with Modrinth’s API, **Theseus Core** is your launchpad into a new era of Minecraft gameplay.
|
||||||
Introducing AstralRinth, a specialized variant of Theseus dedicated to implementing offline authorization for an even more flexible and user-centric Minecraft Modrinth experience. Roam the Minecraft realms without the constraints of online authentication, thanks to AstralRinth.
|
|
||||||
|
|
||||||
## AR • Unlocking Minecraft's Boundless Horizon
|
- *Recently, improved integration with the Git Astralium API has been added.*
|
||||||
Dive into the extraordinary world of AstralRinth, a fork of the original project with a unique focus on providing a free trial experience for Minecraft, all without the need for a license. Currently boasting:
|
|
||||||
|
|
||||||
# Install instructions
|
## **About the Software**
|
||||||
- To install our application, you need to download a file for your operating system from our available releases or development builds • [Download variants here](https://git.astralium.su/didirus/AstralRinth/releases)
|
|
||||||
- After you have downloaded the required executable file or archive, then open it
|
|
||||||
|
|
||||||
### Downloadable file extensions
|
**AstralRinth** is a dedicated branch of the Theseus project, focused on **offline authentication**, offering you more flexibility and control. Play Minecraft without the need for constant online verification — a user-first approach to modern modded gaming.
|
||||||
- `.msi` format for Windows OS system _(Supported popular latest versions of Microsoft Windows)_
|
|
||||||
- `.dmg` format for MacOS system _(Works on Macos Ventura / Sonoma / Sequoia, but it should be works on older OS builds)_
|
|
||||||
- `.deb` format for Linux OS systems _(Since there are quite a few distributions, we do not guarantee
|
|
||||||
|
|
||||||
### Installation subjects
|
## **AR • Unlocking Minecraft's Boundless Horizon**
|
||||||
- Builds in releases that are signed with the following prefixes are not recommended for installation and may contain errors:
|
|
||||||
- `dev`
|
This unique fork introduces a **free trial Minecraft experience**, bypassing license checks while maintaining rich functionality. Currently includes:
|
||||||
- `nightly`
|
|
||||||
- `dirty`
|
---
|
||||||
- `dirty-dev`
|
|
||||||
- `dirty-nightly`
|
# Install Instructions
|
||||||
- `dirty_dev`
|
|
||||||
- `dirty_nightly`
|
To install the launcher:
|
||||||
- Auto-updating takes place through parsing special versions from releases, so we also distribute clean types of `.msi, .dmg and .deb`
|
|
||||||
|
1. Visit the [releases page](https://git.astralium.su/didirus/AstralRinth/releases) to download the correct version for your system.
|
||||||
|
2. Run the downloaded file or extract and launch it, depending on the format.
|
||||||
|
|
||||||
|
### Downloadable File Extensions
|
||||||
|
|
||||||
|
| Extension | OS | Notes |
|
||||||
|
| --------- | ------- | --------------------------------------------------------------------- |
|
||||||
|
| `.msi` | Windows | Supported on all recent Windows versions |
|
||||||
|
| `.dmg` | macOS | Works on Ventura, Sonoma, Sequoia _(may also support older versions)_ |
|
||||||
|
| `.deb` | Linux | Basic support; compatibility may vary by distribution |
|
||||||
|
|
||||||
|
### Installation Warnings
|
||||||
|
|
||||||
|
Avoid using builds with these prefixes — they may be unstable or experimental:
|
||||||
|
|
||||||
|
- `dev`
|
||||||
|
- `nightly`
|
||||||
|
- `dirty`
|
||||||
|
- `dirty-dev`
|
||||||
|
- `dirty-nightly`
|
||||||
|
- `dirty_dev`
|
||||||
|
- `dirty_nightly`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
### Featured enhancement in AR
|
> _The launcher provides an opportunity to use the well-known Modrinth, but with an improved user experience._
|
||||||
- AstralRinth offers a range of authorization options, giving users the flexibility to log in with valid licenses or even a pirate account without auth credentials breaks (_Unlike MultiMC Cracked and similar software_). Experience Minecraft on your terms, breaking free from traditional licensing constraints (_Popular in Russian Federation_).
|
|
||||||
|
|
||||||
### Easy to use
|
## Included exclusive features
|
||||||
- Using the launcher is intuitive, any user can figure it out.
|
|
||||||
|
|
||||||
### Update notifies
|
- No ads in the entire launcher.
|
||||||
- We have implemented notifications about the release of new updates on our Git. The launcher can also download them for you and try to install them.
|
- Custom `.svg` vector icons for a distinct UI.
|
||||||
|
- Improved compatibility with both licensed and pirate accounts.
|
||||||
|
- Use **official microsoft accounts** or **offline/pirate accounts** — login won't break.
|
||||||
|
- Supports license-free access for testing or personal use.
|
||||||
|
- No dependence on official authentication services.
|
||||||
|
- Discord Rich Presence integration:
|
||||||
|
- Dynamic status messages.
|
||||||
|
- In-game timer and AFK counter.
|
||||||
|
- Strict disabling of statistics and other Modrinth metrics.
|
||||||
|
- Optimized archive/package size.
|
||||||
|
- Integrated update fetcher for seamless version management.
|
||||||
|
- Built-in update alerts for new versions posted on Git Astralium.
|
||||||
|
- Automatic download and installation capabilities.
|
||||||
|
- Database migration fixes, when error occurred (Interactive Mode) (Modrinth issue)
|
||||||
|
- ElyBy skin system integration (AuthLib / Java)
|
||||||
|
|
||||||
### Enhancements
|
---
|
||||||
- Custom .SVG vectors for a personalized touch.
|
|
||||||
- Improved compatibility for both pirate and licensed accounts.
|
|
||||||
- Beautiful Discord RPC with random messages while playing, along with an in-game timer and AFK counter.
|
|
||||||
- Forced disabling of statistics collection (modrinch metrics) with a hard patch from AstralRinth, ensuring it remains deactivated regardless of the configuration setting.
|
|
||||||
- Removal of advertisements from all launcher views.
|
|
||||||
- Optimization of packages (archives).
|
|
||||||
- Integrated update fetching feature
|
|
||||||
|
|
||||||
# Getting Started
|
# Getting Started
|
||||||
To begin your AstralRinth adventure, follow these steps:
|
|
||||||
1. **Download Your OS Version**: Head over to our [releases page](https://git.astralium.su/didirus/AstralRinth/releases/) to find the right file for your operating system.
|
To begin using AstralRinth:
|
||||||
- **Choosing the Correct File**: Ensure you select the file that matches your OS requirements.
|
|
||||||
- [**How select file**](#downloadable-file-extensions)
|
1. **Download Your OS Version**
|
||||||
- [**How select release**](#installation-subjects)
|
|
||||||
2. **Authentication**: Log in with a valid license or, for testing, try using a pirate account to see AstralRinth in action.
|
- Go to the [releases page](https://git.astralium.su/didirus/AstralRinth/releases)
|
||||||
3. **Launch Minecraft**: Start your journey by launching Minecraft through AstralRinth and enjoy the adventures that await.
|
- [How to choose a file](#downloadable-file-extensions)
|
||||||
- **Choosing java installation**: The launcher will try to automatically detect the recommended JVM version for running the game, but you can configure everything in the launcher settings.
|
- [How to choose a release](#installation-warnings)
|
||||||
|
|
||||||
|
2. **Log In**
|
||||||
|
|
||||||
|
- Use your official Mojang/Microsoft account, or test using a non-licensed account.
|
||||||
|
|
||||||
|
3. **Launch Minecraft**
|
||||||
|
- Start Minecraft from the launcher.
|
||||||
|
- The launcher will auto-detect the recommended JVM version.
|
||||||
|
- You can also configure Java manually in the settings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Disclaimer
|
# Disclaimer
|
||||||
- AstralRinth is a project intended for experimentation and educational purposes only. It does not endorse or support piracy, and users are encouraged to obtain valid licenses for a fully-supported Minecraft experience.
|
|
||||||
- Users are reminded to respect licensing agreements and support the developers of Minecraft.
|
|
||||||
|
|
||||||
# Support our Project (Crypto Wallets)
|
- **AstralRinth** is intended **solely for educational and experimental use**.
|
||||||
|
- We **do not condone piracy** — users are encouraged to purchase a legitimate Minecraft license.
|
||||||
|
- Respect all relevant licensing agreements and support Minecraft developers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Support Our Project (Crypto Wallets)
|
||||||
|
|
||||||
|
If you'd like to support development, you can donate via the following crypto wallets:
|
||||||
|
|
||||||
- BTC (Telegram): 14g6asNYzcUoaQtB8B2QGKabgEvn55wfLj
|
- BTC (Telegram): 14g6asNYzcUoaQtB8B2QGKabgEvn55wfLj
|
||||||
- USDT TRC20 (Telegram): TMSmv1D5Fdf4fipUpwBCdh16WevrV45vGr
|
- TONCOIN (Telegram): UQAqUJ2_hVBI6k_gPyfp_jd-1K0OS61nIFPZuJWN9BwGAvKe
|
||||||
- TONCOIN (Telegram): UQAqUJ2_hVBI6k_gPyfp_jd-1K0OS61nIFPZuJWN9BwGAvKe
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
|
|||||||
import { handleError, useNotifications } from '@/store/notifications.js'
|
import { handleError, useNotifications } from '@/store/notifications.js'
|
||||||
import { command_listener, warning_listener } from '@/helpers/events.js'
|
import { command_listener, warning_listener } from '@/helpers/events.js'
|
||||||
import { type } from '@tauri-apps/plugin-os'
|
import { type } from '@tauri-apps/plugin-os'
|
||||||
import { getOS, isDev, restartApp } from '@/helpers/utils.js'
|
import { getOS, isDev } from '@/helpers/utils.js'
|
||||||
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
|
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
@@ -72,6 +72,9 @@ import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
|||||||
import { get_available_capes, get_available_skins } from './helpers/skins'
|
import { get_available_capes, get_available_skins } from './helpers/skins'
|
||||||
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
||||||
|
|
||||||
|
// [AR] Feature
|
||||||
|
import { getRemote, updateState } from '@/helpers/update.js'
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
const news = ref([])
|
const news = ref([])
|
||||||
@@ -99,6 +102,7 @@ const isMaximized = ref(false)
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await useCheckDisableMouseover()
|
await useCheckDisableMouseover()
|
||||||
|
await getRemote(false) // [AR] Check for updates
|
||||||
|
|
||||||
document.querySelector('body').addEventListener('click', handleClick)
|
document.querySelector('body').addEventListener('click', handleClick)
|
||||||
document.querySelector('body').addEventListener('auxclick', handleAuxClick)
|
document.querySelector('body').addEventListener('auxclick', handleAuxClick)
|
||||||
@@ -161,11 +165,11 @@ async function setupApp() {
|
|||||||
|
|
||||||
initAnalytics()
|
initAnalytics()
|
||||||
if (!telemetry) {
|
if (!telemetry) {
|
||||||
console.info("[AR] Telemetry disabled by default (Hard patched).")
|
console.info("[AR] • Telemetry disabled by default (Hard patched).")
|
||||||
optOutAnalytics()
|
optOutAnalytics()
|
||||||
}
|
}
|
||||||
if (!personalized_ads) {
|
if (!personalized_ads) {
|
||||||
console.info("[AR] Personalized ads disabled by default (Hard patched).")
|
console.info("[AR] • Personalized ads disabled by default (Hard patched).")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dev) debugAnalytics()
|
if (dev) debugAnalytics()
|
||||||
@@ -188,7 +192,7 @@ async function setupApp() {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Patched by AstralRinth
|
/// [AR] Patch
|
||||||
// useFetch(
|
// useFetch(
|
||||||
// `https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`,
|
// `https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`,
|
||||||
// 'criticalAnnouncements',
|
// 'criticalAnnouncements',
|
||||||
@@ -465,12 +469,20 @@ function handleAuxClick(e) {
|
|||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</NavButton>
|
</NavButton>
|
||||||
<div class="flex flex-grow"></div>
|
<div class="flex flex-grow"></div>
|
||||||
<NavButton v-if="updateAvailable" v-tooltip.right="'Install update'" :to="() => restartApp()">
|
<!-- [AR] TODO -->
|
||||||
|
<!-- <NavButton v-if="updateAvailable" v-tooltip.right="'Install update'" :to="() => restartApp()">
|
||||||
<DownloadIcon />
|
<DownloadIcon />
|
||||||
</NavButton>
|
</NavButton> -->
|
||||||
<NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
|
<template v-if="updateState">
|
||||||
<SettingsIcon />
|
<NavButton class="neon-icon pulse" v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
|
||||||
</NavButton>
|
<SettingsIcon />
|
||||||
|
</NavButton>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
|
||||||
|
<SettingsIcon />
|
||||||
|
</NavButton>
|
||||||
|
</template>
|
||||||
<ButtonStyled v-if="credentials" type="transparent" circular>
|
<ButtonStyled v-if="credentials" type="transparent" circular>
|
||||||
<OverflowMenu
|
<OverflowMenu
|
||||||
:options="[
|
:options="[
|
||||||
@@ -501,13 +513,13 @@ function handleAuxClick(e) {
|
|||||||
<!-- <ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" /> -->
|
<!-- <ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" /> -->
|
||||||
<div class="flex items-center gap-1 ml-3">
|
<div class="flex items-center gap-1 ml-3">
|
||||||
<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.back()"
|
@click="router.back()"
|
||||||
>
|
>
|
||||||
<LeftArrowIcon />
|
<LeftArrowIcon />
|
||||||
</button>
|
</button>
|
||||||
<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()"
|
@click="router.forward()"
|
||||||
>
|
>
|
||||||
<RightArrowIcon />
|
<RightArrowIcon />
|
||||||
@@ -659,6 +671,9 @@ function handleAuxClick(e) {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../packages/assets/styles/neon-icon.scss';
|
||||||
|
@import '../../../packages/assets/styles/neon-text.scss';
|
||||||
|
|
||||||
.window-controls {
|
.window-controls {
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ const filteredResults = computed(() => {
|
|||||||
|
|
||||||
if (sortBy.value === 'Game version') {
|
if (sortBy.value === 'Game version') {
|
||||||
instances.sort((a, b) => {
|
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])
|
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
|
return instanceMap
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div v-if="mode !== 'isolated'" ref="button"
|
||||||
v-if="mode !== 'isolated'"
|
|
||||||
ref="button"
|
|
||||||
class="button-base mt-2 px-3 py-2 bg-button-bg rounded-xl flex items-center gap-2"
|
class="button-base mt-2 px-3 py-2 bg-button-bg rounded-xl flex items-center gap-2"
|
||||||
:class="{ expanded: mode === 'expanded' }"
|
:class="{ expanded: mode === 'expanded' }" @click="toggleMenu">
|
||||||
@click="toggleMenu"
|
<Avatar size="36px" :src="selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||||
>
|
" />
|
||||||
<Avatar
|
|
||||||
size="36px"
|
|
||||||
:src="
|
|
||||||
selectedAccount ? avatarUrl : 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<div class="flex flex-col w-full">
|
<div class="flex flex-col w-full">
|
||||||
<span>
|
<span>
|
||||||
<component :is="getAccountType(selectedAccount)" v-if="selectedAccount" class="vector-icon" />
|
<component :is="getAccountType(selectedAccount)" v-if="selectedAccount" class="vector-icon" />
|
||||||
@@ -32,31 +24,23 @@
|
|||||||
</h4>
|
</h4>
|
||||||
<p>Selected</p>
|
<p>Selected</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button v-tooltip="'Log out'" icon-only color="raised" @click="logout(selectedAccount.profile.id)">
|
||||||
v-tooltip="'Log out'"
|
|
||||||
icon-only
|
|
||||||
color="raised"
|
|
||||||
@click="logout(selectedAccount.profile.id)"
|
|
||||||
>
|
|
||||||
<TrashIcon />
|
<TrashIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="login-section account">
|
<div v-else class="login-section account">
|
||||||
<h4>Not signed in</h4>
|
<h4>Not signed in</h4>
|
||||||
<Button
|
<Button v-tooltip="'Log via Microsoft'" :disabled="microsoftLoginDisabled" icon-only @click="login()">
|
||||||
v-tooltip="'Log in'"
|
<MicrosoftIcon v-if="!microsoftLoginDisabled" />
|
||||||
:disabled="loginDisabled"
|
|
||||||
icon-only
|
|
||||||
color="primary"
|
|
||||||
@click="login()"
|
|
||||||
>
|
|
||||||
<LogInIcon v-if="!loginDisabled" />
|
|
||||||
<SpinnerIcon v-else class="animate-spin" />
|
<SpinnerIcon v-else class="animate-spin" />
|
||||||
<MicrosoftIcon/>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'Add offline'" icon-only @click="tryOfflineLogin()">
|
<Button v-tooltip="'Add offline account'" icon-only @click="showOfflineLoginModal()">
|
||||||
<PirateIcon />
|
<PirateIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button v-tooltip="'Log via Ely.by'" icon-only @click="showElybyLoginModal()">
|
||||||
|
<ElyByIcon v-if="!elybyLoginDisabled" />
|
||||||
|
<SpinnerIcon v-else class="animate-spin" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="displayAccounts.length > 0" class="account-group">
|
<div v-if="displayAccounts.length > 0" class="account-group">
|
||||||
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
||||||
@@ -73,53 +57,135 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="accounts.length > 0" class="login-section account centered">
|
<div v-if="accounts.length > 0" class="login-section account centered">
|
||||||
<Button v-tooltip="'Log in'" icon-only @click="login()">
|
<Button v-tooltip="'Log via Microsoft'" icon-only @click="login()">
|
||||||
<MicrosoftIcon />
|
<MicrosoftIcon v-if="!microsoftLoginDisabled" />
|
||||||
|
<SpinnerIcon v-else class="animate-spin" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-tooltip="'Add offline'" icon-only @click="tryOfflineLogin()">
|
<Button v-tooltip="'Add offline account'" icon-only @click="showOfflineLoginModal()">
|
||||||
<PirateIcon />
|
<PirateIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button v-tooltip="'Log via Ely.by'" icon-only @click="showElybyLoginModal()">
|
||||||
|
<ElyByIcon v-if="!elybyLoginDisabled" />
|
||||||
|
<SpinnerIcon v-else class="animate-spin" />
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</transition>
|
</transition>
|
||||||
<ModalWrapper ref="loginOfflineModal" class="modal" header="Add new offline account">
|
<ModalWrapper ref="addElybyModal" class="modal" header="Authenticate with Ely.by">
|
||||||
<div class="modal-body">
|
<ModalWrapper ref="requestElybyTwoFactorCodeModal" class="modal"
|
||||||
<div class="label">Enter offline username</div>
|
header="Ely.by requested 2FA code for authentication">
|
||||||
<input type="text" v-model="playerName" placeholder="Provide offline player name" />
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
<Button icon-only color="secondary" @click="offlineLoginFinally()">
|
<label class="label">Enter your 2FA code</label>
|
||||||
Continue
|
<input v-model="elybyTwoFactorCode" type="text" placeholder="Your 2FA code here..." class="input" />
|
||||||
</Button>
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button icon-only color="primary" class="continue-button" @click="addElybyProfile()">
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="label">Enter your player name or email (preferred)</label>
|
||||||
|
<input v-model="elybyLogin" type="text" placeholder="Your player name or email here..." class="input" />
|
||||||
|
<label class="label">Enter your password</label>
|
||||||
|
<input v-model="elybyPassword" type="password" placeholder="Your password here..." class="input" />
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button icon-only color="primary" class="continue-button" @click="addElybyProfile()">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
<ModalWrapper ref="loginErrorModal" class="modal" header="Error while proceed">
|
<ModalWrapper ref="addOfflineModal" class="modal" header="Add new offline account">
|
||||||
<div class="modal-body">
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
<div class="label">Error occurred while adding offline account</div>
|
<label class="label">Enter your player name</label>
|
||||||
<Button color="primary" @click="retryOfflineLogin()">
|
<input v-model="offlinePlayerName" type="text" placeholder="Your player name here..." class="input" />
|
||||||
Try again
|
<div class="mt-6 ml-auto">
|
||||||
</Button>
|
<Button icon-only color="primary" class="continue-button" @click="addOfflineProfile()">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
<ModalWrapper ref="unexpectedErrorModal" class="modal" header="Ошибка">
|
<ModalWrapper
|
||||||
|
ref="authenticationElybyErrorModal"
|
||||||
|
class="modal"
|
||||||
|
header="Error while proceeding authentication event with Ely.by">
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="text-base font-medium text-red-700">
|
||||||
|
An error occurred while logging in.
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" class="retry-button" @click="retryAddElybyProfile">
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper ref="inputElybyErrorModal" class="modal" header="Error while proceeding input event with Ely.by">
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="text-base font-medium text-red-700">
|
||||||
|
An error occurred while adding the Ely.by account. Please follow the instructions below.
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<ul class="list-disc list-inside text-sm space-y-1">
|
||||||
|
<li>Check that you have entered the correct player name or email.</li>
|
||||||
|
<li>Check that you have entered the correct password.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" class="retry-button" @click="retryAddElybyProfile">
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper ref="inputErrorModal" class="modal" header="Error while proceeding input event with offline account">
|
||||||
|
<div class="flex flex-col gap-4 px-6 py-5">
|
||||||
|
<label class="text-base font-medium text-red-700">
|
||||||
|
An error occurred while adding the offline account. Please follow the instructions below.
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<ul class="list-disc list-inside text-sm space-y-1">
|
||||||
|
<li>Check that you have entered the correct player name.</li>
|
||||||
|
<li>
|
||||||
|
Player name must be at least {{ minOfflinePlayerNameLength }} characters long and no more than
|
||||||
|
{{ maxOfflinePlayerNameLength }} characters.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="mt-6 ml-auto">
|
||||||
|
<Button color="primary" class="retry-button" @click="retryAddOfflineProfile">
|
||||||
|
Try again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper ref="exceptionErrorModal" class="modal" header="Unexpected error occurred">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="label">Unexcepted error</div>
|
<label class="label">An unexpected error has occurred. Please try again later.</label>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
DropdownIcon,
|
DropdownIcon,
|
||||||
PlusIcon,
|
|
||||||
TrashIcon,
|
TrashIcon,
|
||||||
LogInIcon,
|
|
||||||
PirateIcon as Offline,
|
PirateIcon as Offline,
|
||||||
MicrosoftIcon as License,
|
MicrosoftIcon as License,
|
||||||
|
ElyByIcon as Elyby,
|
||||||
MicrosoftIcon,
|
MicrosoftIcon,
|
||||||
PirateIcon,
|
PirateIcon,
|
||||||
SpinnerIcon } from '@modrinth/assets'
|
ElyByIcon,
|
||||||
|
SpinnerIcon
|
||||||
|
} from '@modrinth/assets'
|
||||||
import { Avatar, Button, Card } from '@modrinth/ui'
|
import { Avatar, Button, Card } from '@modrinth/ui'
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
||||||
import {
|
import {
|
||||||
|
elyby_auth_authenticate,
|
||||||
|
elyby_login,
|
||||||
offline_login,
|
offline_login,
|
||||||
users,
|
users,
|
||||||
remove_user,
|
remove_user,
|
||||||
@@ -146,48 +212,180 @@ defineProps({
|
|||||||
const emit = defineEmits(['change'])
|
const emit = defineEmits(['change'])
|
||||||
|
|
||||||
const accounts = ref({})
|
const accounts = ref({})
|
||||||
const loginDisabled = ref(false)
|
const microsoftLoginDisabled = ref(false)
|
||||||
|
const elybyLoginDisabled = ref(false)
|
||||||
const defaultUser = ref()
|
const defaultUser = ref()
|
||||||
const loginOfflineModal = ref(null)
|
|
||||||
const loginErrorModal = ref(null)
|
|
||||||
const unexpectedErrorModal = ref(null)
|
|
||||||
const playerName = ref('')
|
|
||||||
|
|
||||||
async function tryOfflineLogin() { // Patched by AstralRinth
|
// [AR] • Feature
|
||||||
loginOfflineModal.value.show()
|
const clientToken = "astralrinth"
|
||||||
|
const addOfflineModal = ref(null)
|
||||||
|
const addElybyModal = ref(null)
|
||||||
|
const requestElybyTwoFactorCodeModal = ref(null)
|
||||||
|
const authenticationElybyErrorModal = ref(null)
|
||||||
|
const inputElybyErrorModal = ref(null)
|
||||||
|
const inputErrorModal = ref(null)
|
||||||
|
const exceptionErrorModal = ref(null)
|
||||||
|
const offlinePlayerName = ref('')
|
||||||
|
const elybyLogin = ref('')
|
||||||
|
const elybyPassword = ref('')
|
||||||
|
const elybyTwoFactorCode = ref('')
|
||||||
|
const minOfflinePlayerNameLength = 2
|
||||||
|
const maxOfflinePlayerNameLength = 20
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function getAccountType(account) {
|
||||||
|
switch (account.account_type) {
|
||||||
|
case 'microsoft':
|
||||||
|
return License
|
||||||
|
case 'pirate':
|
||||||
|
return Offline
|
||||||
|
case 'elyby':
|
||||||
|
return Elyby
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function offlineLoginFinally() { // Patched by AstralRinth
|
// [AR] • Feature
|
||||||
const name = playerName.value
|
function showOfflineLoginModal() {
|
||||||
if (name.length > 1 && name.length < 20 && name !== '') {
|
addOfflineModal.value?.show()
|
||||||
const loggedIn = await offline_login(name).catch(handleError)
|
}
|
||||||
loginOfflineModal.value.hide()
|
|
||||||
if (loggedIn) {
|
// [AR] • Feature
|
||||||
await setAccount(loggedIn)
|
function showElybyLoginModal() {
|
||||||
|
addElybyModal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function retryAddOfflineProfile() {
|
||||||
|
inputErrorModal.value?.hide()
|
||||||
|
clearOfflineFields()
|
||||||
|
showOfflineLoginModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function retryAddElybyProfile() {
|
||||||
|
authenticationElybyErrorModal.value?.hide()
|
||||||
|
inputElybyErrorModal.value?.hide()
|
||||||
|
clearElybyFields()
|
||||||
|
showElybyLoginModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function clearElybyFields() {
|
||||||
|
elybyLogin.value = ''
|
||||||
|
elybyPassword.value = ''
|
||||||
|
elybyTwoFactorCode.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function clearOfflineFields() {
|
||||||
|
offlinePlayerName.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
async function addOfflineProfile() {
|
||||||
|
const name = offlinePlayerName.value.trim()
|
||||||
|
const isValidName = name.length >= minOfflinePlayerNameLength && name.length <= maxOfflinePlayerNameLength
|
||||||
|
|
||||||
|
if (!isValidName) {
|
||||||
|
addOfflineModal.value?.hide()
|
||||||
|
inputErrorModal.value?.show()
|
||||||
|
clearOfflineFields()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await offline_login(name)
|
||||||
|
|
||||||
|
addOfflineModal.value?.hide()
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
await setAccount(result)
|
||||||
await refreshValues()
|
await refreshValues()
|
||||||
} else {
|
} else {
|
||||||
unexpectedErrorModal.value.show()
|
exceptionErrorModal.value?.show()
|
||||||
}
|
}
|
||||||
playerName.value = ''
|
} catch (error) {
|
||||||
} else {
|
handleError(error)
|
||||||
playerName.value = ''
|
exceptionErrorModal.value?.show()
|
||||||
loginOfflineModal.value.hide()
|
} finally {
|
||||||
loginErrorModal.value.show()
|
clearOfflineFields()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function retryOfflineLogin() { // Patched by AstralRinth
|
// [AR] • Feature
|
||||||
loginErrorModal.value.hide()
|
async function addElybyProfile() {
|
||||||
tryOfflineLogin()
|
if (!elybyLogin.value || !elybyPassword.value) {
|
||||||
}
|
addElybyModal.value?.hide()
|
||||||
|
inputElybyErrorModal.value?.show()
|
||||||
|
clearElybyFields()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
elybyLoginDisabled.value = true
|
||||||
|
|
||||||
function getAccountType(account) { // Patched by AstralRinth
|
const login = elybyLogin.value.trim()
|
||||||
if (account.access_token != "null" && account.access_token != null && account.access_token != "") {
|
let password = elybyPassword.value.trim()
|
||||||
return License
|
const twoFactorCode = elybyTwoFactorCode.value.trim()
|
||||||
} else {
|
if (password && twoFactorCode) {
|
||||||
return Offline
|
password = `${password}:${twoFactorCode}`
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw_result = await elyby_auth_authenticate(
|
||||||
|
login,
|
||||||
|
password,
|
||||||
|
clientToken
|
||||||
|
)
|
||||||
|
|
||||||
|
const json_data = JSON.parse(raw_result)
|
||||||
|
|
||||||
|
console.log(json_data?.error)
|
||||||
|
console.log(json_data?.errorMessage)
|
||||||
|
|
||||||
|
if (!json_data.accessToken) {
|
||||||
|
if (
|
||||||
|
json_data.error === 'ForbiddenOperationException' &&
|
||||||
|
json_data.errorMessage?.includes('two factor')
|
||||||
|
) {
|
||||||
|
requestElybyTwoFactorCodeModal.value?.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addElybyModal.value?.hide()
|
||||||
|
requestElybyTwoFactorCodeModal.value?.hide()
|
||||||
|
authenticationElybyErrorModal.value?.show()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessToken = json_data.accessToken
|
||||||
|
const selectedProfileId = convertRawStringToUUIDv4(json_data.selectedProfile.id)
|
||||||
|
const selectedProfileName = json_data.selectedProfile.name
|
||||||
|
|
||||||
|
const result = await elyby_login(selectedProfileId, selectedProfileName, accessToken)
|
||||||
|
|
||||||
|
addElybyModal.value?.hide()
|
||||||
|
requestElybyTwoFactorCodeModal.value?.hide()
|
||||||
|
|
||||||
|
clearElybyFields()
|
||||||
|
|
||||||
|
await setAccount(result)
|
||||||
|
await refreshValues()
|
||||||
|
} catch (err) {
|
||||||
|
handleError(err)
|
||||||
|
exceptionErrorModal.value?.show()
|
||||||
|
} finally {
|
||||||
|
elybyLoginDisabled.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
function convertRawStringToUUIDv4(rawId) {
|
||||||
|
if (rawId.length !== 32) {
|
||||||
|
console.warn('Invalid UUID string:', rawId)
|
||||||
|
return rawId
|
||||||
|
}
|
||||||
|
return `${rawId.slice(0, 8)}-${rawId.slice(8, 12)}-${rawId.slice(12, 16)}-${rawId.slice(16, 20)}-${rawId.slice(20)}`
|
||||||
|
}
|
||||||
|
|
||||||
const equippedSkin = ref(null)
|
const equippedSkin = ref(null)
|
||||||
const headUrlCache = ref(new Map())
|
const headUrlCache = ref(new Map())
|
||||||
|
|
||||||
@@ -213,13 +411,13 @@ async function refreshValues() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setLoginDisabled(value) {
|
function setLoginDisabled(value) {
|
||||||
loginDisabled.value = value
|
microsoftLoginDisabled.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
refreshValues,
|
refreshValues,
|
||||||
setLoginDisabled,
|
setLoginDisabled,
|
||||||
loginDisabled,
|
loginDisabled: microsoftLoginDisabled,
|
||||||
})
|
})
|
||||||
await refreshValues()
|
await refreshValues()
|
||||||
|
|
||||||
@@ -265,7 +463,7 @@ async function setAccount(account) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
loginDisabled.value = true
|
microsoftLoginDisabled.value = true
|
||||||
const loggedIn = await login_flow().catch(handleSevereError)
|
const loggedIn = await login_flow().catch(handleSevereError)
|
||||||
|
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
@@ -274,7 +472,7 @@ async function login() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
trackEvent('AccountLogIn')
|
trackEvent('AccountLogIn')
|
||||||
loginDisabled.value = false
|
microsoftLoginDisabled.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const logout = async (id) => {
|
const logout = async (id) => {
|
||||||
|
|||||||
@@ -18,11 +18,16 @@ import { cancel_directory_change } from '@/helpers/settings.ts'
|
|||||||
import { install } from '@/helpers/profile.js'
|
import { install } from '@/helpers/profile.js'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
|
import { applyMigrationFix } from '@/helpers/utils.js'
|
||||||
|
import { restartApp } from '@/helpers/utils.js'
|
||||||
|
|
||||||
const errorModal = ref()
|
const errorModal = ref()
|
||||||
const error = ref()
|
const error = ref()
|
||||||
const closable = ref(true)
|
const closable = ref(true)
|
||||||
const errorCollapsed = ref(false)
|
const errorCollapsed = ref(false)
|
||||||
|
const language = ref('en')
|
||||||
|
const migrationFixSuccess = ref(null) // null | true | false
|
||||||
|
const migrationFixCallbackModel = ref()
|
||||||
|
|
||||||
const title = ref('An error occurred')
|
const title = ref('An error occurred')
|
||||||
const errorType = ref('unknown')
|
const errorType = ref('unknown')
|
||||||
@@ -148,6 +153,30 @@ async function copyToClipboard(text) {
|
|||||||
copied.value = false
|
copied.value = false
|
||||||
}, 3000)
|
}, 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleLanguage() {
|
||||||
|
language.value = language.value === 'en' ? 'ru' : 'en'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onApplyMigrationFix(eol) {
|
||||||
|
console.log(`[AR] • Attempting to apply migration ${eol.toUpperCase()} fix`)
|
||||||
|
try {
|
||||||
|
const result = await applyMigrationFix(eol)
|
||||||
|
migrationFixSuccess.value = result === true
|
||||||
|
console.log(`[AR] • Successfully applied migration ${eol.toUpperCase()} fix`, result)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[AR] • Failed to apply migration fix:`, err)
|
||||||
|
migrationFixSuccess.value = false
|
||||||
|
} finally {
|
||||||
|
migrationFixCallbackModel.value?.show?.()
|
||||||
|
if (migrationFixSuccess.value === true) {
|
||||||
|
setTimeout(async () => {
|
||||||
|
await restartApp()
|
||||||
|
}, 3000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -298,10 +327,20 @@ async function copyToClipboard(text) {
|
|||||||
<template v-if="copied"> <CheckIcon class="text-green" /> Copied! </template>
|
<template v-if="copied"> <CheckIcon class="text-green" /> Copied! </template>
|
||||||
<template v-else> <CopyIcon /> Copy debug info </template>
|
<template v-else> <CopyIcon /> Copy debug info </template>
|
||||||
</button>
|
</button>
|
||||||
|
<ButtonStyled class="neon-button neon">
|
||||||
|
<a href="https://me.astralium.su/get/ar/help" target="_blank" rel="noopener noreferrer">
|
||||||
|
Get AstralRinth support
|
||||||
|
</a>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled class="neon-button neon" >
|
||||||
|
<a href="https://me.astralium.su/get/ar" target="_blank" rel="noopener noreferrer">
|
||||||
|
Checkout latest releases
|
||||||
|
</a>
|
||||||
|
</ButtonStyled>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="hasDebugInfo">
|
<template v-if="hasDebugInfo">
|
||||||
<div class="bg-button-bg rounded-xl mt-2 overflow-clip">
|
<div class="bg-button-bg rounded-xl mt-2 overflow-hidden">
|
||||||
<button
|
<button
|
||||||
class="flex items-center justify-between w-full bg-transparent border-0 px-4 py-3 cursor-pointer"
|
class="flex items-center justify-between w-full bg-transparent border-0 px-4 py-3 cursor-pointer"
|
||||||
@click="errorCollapsed = !errorCollapsed"
|
@click="errorCollapsed = !errorCollapsed"
|
||||||
@@ -313,12 +352,123 @@ async function copyToClipboard(text) {
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<Collapsible :collapsed="errorCollapsed">
|
<Collapsible :collapsed="errorCollapsed">
|
||||||
<pre class="m-0 px-4 py-3 bg-bg rounded-none">{{ debugInfo }}</pre>
|
<pre
|
||||||
|
class="m-0 px-4 py-3 bg-bg rounded-none whitespace-pre-wrap break-words overflow-x-auto max-w-full"
|
||||||
|
>{{ debugInfo }}</pre>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-if="errorType === 'state_init'">
|
||||||
|
<div class="notice">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h3 v-if="language === 'en'" class="notice__title">⚠️ Migration Issue • Important Notice ⚠️</h3>
|
||||||
|
<h3 v-if="language === 'ru'" class="notice__title">⚠️ Проблема миграции • Важное уведомление ⚠️</h3>
|
||||||
|
<ButtonStyled>
|
||||||
|
<button @click="toggleLanguage">
|
||||||
|
{{ language === 'en' ? '📖 Русский' : '📖 English' }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
<p v-if="language === 'en'" class="notice__text">
|
||||||
|
We're experiencing an issue with our database migration system due to differences in how different operating systems handle line endings. This might cause problems with our app's functionality.
|
||||||
|
</p>
|
||||||
|
<p v-if="language === 'en'" class="notice__text">
|
||||||
|
<strong>What's happening?</strong> When we build our app, we use a system that checks the integrity of our database migrations. However, this system can get confused when it encounters different line endings (like CRLF vs LF) used by different operating systems. This can lead to errors and make our app unusable.
|
||||||
|
</p>
|
||||||
|
<p v-if="language === 'en'" class="notice__text">
|
||||||
|
<strong>Why is this happening?</strong> This issue is caused by a combination of factors, including different operating systems handling line endings differently, Git's line ending conversion settings, and our app's build process.
|
||||||
|
</p>
|
||||||
|
<p v-if="language === 'en'" class="notice__text">
|
||||||
|
<strong>What are we doing about it?</strong> We're working to resolve this issue and ensure that our app works smoothly for all users. In the meantime, we apologize for any inconvenience this might cause and appreciate your patience and understanding.
|
||||||
|
</p>
|
||||||
|
<p v-if="language === 'ru'" class="notice__text">
|
||||||
|
Мы сталкиваемся с проблемой в нашей системе миграции базы данных из-за различий в том, как разные операционные системы обрабатывают окончания строк. Это может вызвать проблемы с функциональностью нашего приложения.
|
||||||
|
</p>
|
||||||
|
<p v-if="language === 'ru'" class="notice__text">
|
||||||
|
<strong>Что происходит?</strong> Когда мы строим наше приложение, мы используем систему, которая проверяет целостность наших миграций базы данных. Однако эта система может сбиваться, когда сталкивается с различными окончаниями строк (например, CRLF против LF), используемыми разными операционными системами. Это может привести к ошибкам и сделать наше приложение неработоспособным.
|
||||||
|
</p>
|
||||||
|
<p v-if="language === 'ru'" class="notice__text">
|
||||||
|
<strong>Почему это происходит?</strong> Эта проблема вызвана сочетанием факторов, включая различную обработку окончаний строк разными операционными системами, настройки преобразования окончаний строк в Git и процесс сборки нашего приложения.
|
||||||
|
</p>
|
||||||
|
<p v-if="language === 'ru'" class="notice__text">
|
||||||
|
<strong>Что мы с этим делаем?</strong> Мы работаем над решением этой проблемы и обеспечением бесперебойной работы нашего приложения для всех пользователей. В это время мы извиняемся за возможные неудобства и благодарим вас за терпение и понимание.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-lg font-bold text-contrast">
|
||||||
|
<template v-if="language === 'en'">Possible fix in real time:</template>
|
||||||
|
<template v-if="language === 'ru'">Возможное исправление в реальном времени:</template>
|
||||||
|
</h2>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<ol class="flex flex-col gap-3">
|
||||||
|
<li>
|
||||||
|
<ButtonStyled class="neon-button neon">
|
||||||
|
<button
|
||||||
|
:title="language === 'en'
|
||||||
|
? 'Convert all line endings in migration files to LF (Unix-style: \\n)'
|
||||||
|
: 'Преобразовать все окончания строк в файлах миграций в LF (Unix-стиль: \\n)'"
|
||||||
|
aria-label="LF"
|
||||||
|
@click="onApplyMigrationFix('lf')"
|
||||||
|
>
|
||||||
|
{{ language === 'en' ? 'Apply LF Migration Fix' : 'Применить исправление миграции LF' }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ButtonStyled class="neon-button neon">
|
||||||
|
<button
|
||||||
|
:title="language === 'en'
|
||||||
|
? 'Convert all line endings in migration files to CRLF (Windows-style: \\r\\n)'
|
||||||
|
: 'Преобразовать все окончания строк в файлах миграций в CRLF (Windows-стиль: \\r\\n)'"
|
||||||
|
aria-label="CRLF"
|
||||||
|
@click="onApplyMigrationFix('crlf')"
|
||||||
|
>
|
||||||
|
{{ language === 'en' ? 'Apply CRLF Migration Fix' : 'Применить исправление миграции CRLF' }}
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
|
<ModalWrapper
|
||||||
|
ref="migrationFixCallbackModel"
|
||||||
|
:header="language === 'en'
|
||||||
|
? '💡 Migration fix report'
|
||||||
|
: '💡 Отчет об исправлении миграции'"
|
||||||
|
:closable="closable">
|
||||||
|
<div class="modal-body">
|
||||||
|
<h2 class="text-lg font-bold text-contrast space-y-2">
|
||||||
|
<template v-if="migrationFixSuccess === true">
|
||||||
|
<p class="flex items-center gap-2 neon-text">
|
||||||
|
✅
|
||||||
|
{{ language === 'en'
|
||||||
|
? 'The migration fix has been applied successfully. Please restart the launcher and try to log in to the game :)'
|
||||||
|
: 'Исправление миграции успешно применено. Пожалуйста, перезапустите лаунчер и попробуйте снова авторизоваться в игре :)' }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm neon-text">
|
||||||
|
{{ language === 'en'
|
||||||
|
? 'If the problem persists, please try the other fix.'
|
||||||
|
: 'Если проблема сохраняется, пожалуйста, попробуйте другой способ.' }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="migrationFixSuccess === false">
|
||||||
|
<p class="flex items-center gap-2 neon-text">
|
||||||
|
❌
|
||||||
|
{{ language === 'en'
|
||||||
|
? 'The migration fix failed or had no effect.'
|
||||||
|
: 'Исправление миграции не было успешно применено или не имело эффекта.' }}
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm neon-text">
|
||||||
|
{{ language === 'en'
|
||||||
|
? 'If the problem persists, please try the other fix.'
|
||||||
|
: 'Если проблема сохраняется, пожалуйста, попробуйте другой способ.' }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -333,6 +483,9 @@ async function copyToClipboard(text) {
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
@import '../../../../../packages/assets/styles/neon-button.scss';
|
||||||
|
@import '../../../../../packages/assets/styles/neon-text.scss';
|
||||||
|
|
||||||
.cta-button {
|
.cta-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ async function testJava() {
|
|||||||
testingJava.value = true
|
testingJava.value = true
|
||||||
testingJavaSuccess.value = await test_jre(
|
testingJavaSuccess.value = await test_jre(
|
||||||
props.modelValue ? props.modelValue.path : '',
|
props.modelValue ? props.modelValue.path : '',
|
||||||
1,
|
|
||||||
props.version,
|
props.version,
|
||||||
)
|
)
|
||||||
testingJava.value = false
|
testingJava.value = false
|
||||||
|
|||||||
@@ -36,60 +36,6 @@
|
|||||||
<span class="circle stopped" />
|
<span class="circle stopped" />
|
||||||
<span class="running-text"> No instances running </span>
|
<span class="running-text"> No instances running </span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="updateState">
|
|
||||||
<a>
|
|
||||||
<Button class="download" :disabled="installState" @click="initUpdateModal(), getRemote(false)">
|
|
||||||
<DownloadIcon />
|
|
||||||
{{
|
|
||||||
installState
|
|
||||||
? "Downloading new update..."
|
|
||||||
: "Download new update"
|
|
||||||
}}
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<ModalWrapper ref="updateModalView" :has-to-type="false" header="Request to update the AstralRinth launcher">
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="markdown-body">
|
|
||||||
<p>The new version of the AstralRinth launcher is available.</p>
|
|
||||||
<p>Your version is outdated. We recommend that you update to the latest version.</p>
|
|
||||||
<p><strong>⚠️ Warning ⚠️</strong></p>
|
|
||||||
<p>
|
|
||||||
Before updating, make sure that you have saved all running instances and made a backup copy of the instances
|
|
||||||
that are valuable to you. Remember that the authors of the product are not responsible for the breakdown of
|
|
||||||
your files, so you should always make copies of them and keep them in a safe place.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<span>Source • Git Astralium</span>
|
|
||||||
<span>Version on remote server • <p id="releaseData" class="cosmic inline-fix"></p></span>
|
|
||||||
<span>Version on local device •
|
|
||||||
<p class="cosmic inline-fix">v{{ version }}</p>
|
|
||||||
</span>
|
|
||||||
<div class="button-group push-right">
|
|
||||||
<Button class="updater-modal" @click="updateModalView.hide()">
|
|
||||||
Cancel</Button>
|
|
||||||
<Button class="updater-modal" @click="initDownload()">
|
|
||||||
Download file
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
<ModalWrapper ref="updateRequestFailView" :has-to-type="false" header="Failed to request a file from the server :(">
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="markdown-body">
|
|
||||||
<p><strong>Error occurred</strong></p>
|
|
||||||
<p>Unfortunately, the program was unable to download the file from our servers.</p>
|
|
||||||
<p>Please try downloading it yourself from <a href="https://me.astralium.su/get/ar" target="_blank" rel="noopener noreferrer">Git Astralium</a> if there are any updates available.</p>
|
|
||||||
</div>
|
|
||||||
<span>Local AstralRinth •
|
|
||||||
<p class="cosmic inline-fix">v{{ version }}</p>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="button-group push-right">
|
|
||||||
<Button class="updater-modal" @click="updateRequestFailView.hide()">
|
|
||||||
Close</Button>
|
|
||||||
</div>
|
|
||||||
</ModalWrapper>
|
|
||||||
</div>
|
</div>
|
||||||
<transition name="download">
|
<transition name="download">
|
||||||
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
|
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
|
||||||
@@ -138,29 +84,6 @@ import ProgressBar from '@/components/ui/ProgressBar.vue'
|
|||||||
import { handleError } from '@/store/notifications.js'
|
import { handleError } from '@/store/notifications.js'
|
||||||
import { get_many } from '@/helpers/profile.js'
|
import { get_many } from '@/helpers/profile.js'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
|
||||||
|
|
||||||
const version = await getVersion()
|
|
||||||
|
|
||||||
import { installState, getRemote, updateState } from '@/helpers/update.js'
|
|
||||||
import ModalWrapper from './modal/ModalWrapper.vue'
|
|
||||||
|
|
||||||
const updateModalView = ref(null)
|
|
||||||
const updateRequestFailView = ref(null)
|
|
||||||
|
|
||||||
const initUpdateModal = async () => {
|
|
||||||
updateModalView.value.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const initDownload = async () => {
|
|
||||||
updateModalView.value.hide()
|
|
||||||
const result = await getRemote(true);
|
|
||||||
if (!result) {
|
|
||||||
updateRequestFailView.value.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await getRemote(false)
|
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const card = ref(null)
|
const card = ref(null)
|
||||||
@@ -318,101 +241,6 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.inline-fix {
|
|
||||||
display: inline-flex;
|
|
||||||
margin-top: -2rem;
|
|
||||||
margin-bottom: -2rem;
|
|
||||||
//margin-left: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cosmic {
|
|
||||||
color: #3e8cde;
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow:
|
|
||||||
0 0 4px rgba(79, 173, 255, 0.5),
|
|
||||||
0 0 8px rgba(14, 98, 204, 0.5),
|
|
||||||
0 0 12px rgba(122, 31, 199, 0.5);
|
|
||||||
transition: color 0.35s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body {
|
|
||||||
:deep(table) {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(hr),
|
|
||||||
:deep(h1),
|
|
||||||
:deep(h2) {
|
|
||||||
max-width: max(60rem, 90%);
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(ul),
|
|
||||||
:deep(ol) {
|
|
||||||
margin-left: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: var(--gap-lg);
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
color: var(--color-contrast);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.download {
|
|
||||||
color: #3e8cde;
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
border: 1px solid var(--color-button-bg);
|
|
||||||
// padding: var(--gap-sm) var(--gap-lg);
|
|
||||||
background-color: rgba(0, 0, 0, 0);
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow:
|
|
||||||
0 0 4px rgba(79, 173, 255, 0.5),
|
|
||||||
0 0 8px rgba(14, 98, 204, 0.5),
|
|
||||||
0 0 12px rgba(122, 31, 199, 0.5);
|
|
||||||
transition: color 0.35s ease;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.download:hover,
|
|
||||||
.download:focus,
|
|
||||||
.download:active {
|
|
||||||
color: #10fae5;
|
|
||||||
text-shadow: #26065e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.updater-modal {
|
|
||||||
color: #3e8cde;
|
|
||||||
padding: var(--gap-sm) var(--gap-lg);
|
|
||||||
text-decoration: none;
|
|
||||||
text-shadow:
|
|
||||||
0 0 4px rgba(79, 173, 255, 0.5),
|
|
||||||
0 0 8px rgba(14, 98, 204, 0.5),
|
|
||||||
0 0 12px rgba(122, 31, 199, 0.5);
|
|
||||||
transition: color 0.35s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.updater-modal:hover,
|
|
||||||
.updater-modal:focus,
|
|
||||||
.updater-modal:active {
|
|
||||||
color: #10fae5;
|
|
||||||
text-shadow: #26065e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-groups {
|
.action-groups {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
type Version,
|
type Version,
|
||||||
} from '@modrinth/utils'
|
} from '@modrinth/utils'
|
||||||
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
import ConfirmModalWrapper from '@/components/ui/modal/ConfirmModalWrapper.vue'
|
||||||
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import { get_project, get_version_many } from '@/helpers/cache'
|
import { get_project, get_version_many } from '@/helpers/cache'
|
||||||
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
import ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
@@ -35,6 +36,11 @@ import type {
|
|||||||
Manifest,
|
Manifest,
|
||||||
} from '../../../helpers/types'
|
} from '../../../helpers/types'
|
||||||
|
|
||||||
|
import { initAuthlibPatching } from '@/helpers/utils.js'
|
||||||
|
const authLibPatchingModal = ref(null)
|
||||||
|
const isAuthLibPatchedSuccess = ref(false)
|
||||||
|
const isAuthLibPatching = ref(false)
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
const repairConfirmModal = ref()
|
const repairConfirmModal = ref()
|
||||||
@@ -447,9 +453,43 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'reinstall',
|
defaultMessage: 'reinstall',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function handleInitAuthLibPatching(ismojang: boolean) {
|
||||||
|
isAuthLibPatching.value = true
|
||||||
|
let state = false
|
||||||
|
let instance_path = props.instance.loader_version != null ? props.instance.game_version + "-" + props.instance.loader_version : props.instance.game_version
|
||||||
|
try {
|
||||||
|
state = await initAuthlibPatching(instance_path, ismojang)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
|
isAuthLibPatching.value = false
|
||||||
|
isAuthLibPatchedSuccess.value = state
|
||||||
|
authLibPatchingModal.value.show()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<ModalWrapper
|
||||||
|
ref="authLibPatchingModal"
|
||||||
|
:header="'AuthLib installation report'"
|
||||||
|
:closable="true"
|
||||||
|
@close="authLibPatchingModal.hide()"
|
||||||
|
>
|
||||||
|
<div class="modal-body">
|
||||||
|
<h2 class="text-lg font-bold text-contrast space-y-2">
|
||||||
|
<p class="flex items-center gap-2 neon-text">
|
||||||
|
<span v-if="isAuthLibPatchedSuccess" class="neon-text">
|
||||||
|
AuthLib installation completed successfully! Now you can log in and play!
|
||||||
|
</span>
|
||||||
|
<span v-else class="neon-text">
|
||||||
|
Failed to install AuthLib. It's possible that no compatible AuthLib version was found for the selected game and/or mod loader version.
|
||||||
|
There may also be a problem with accessing resources behind CloudFlare.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
<ConfirmModalWrapper
|
<ConfirmModalWrapper
|
||||||
ref="repairConfirmModal"
|
ref="repairConfirmModal"
|
||||||
:title="formatMessage(messages.repairConfirmTitle)"
|
:title="formatMessage(messages.repairConfirmTitle)"
|
||||||
@@ -720,6 +760,24 @@ const messages = defineMessages({
|
|||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
|
<h2 class="m-0 mt-4 text-lg font-extrabold text-contrast block">
|
||||||
|
<div v-if="isAuthLibPatching" class="w-6 h-6 cursor-pointer hover:brightness-75 neon-icon pulse">
|
||||||
|
<SpinnerIcon class="size-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
Auth system (Skins) <span class="text-sm font-bold px-2 bg-brand-highlight text-brand rounded-full">Beta</span>
|
||||||
|
</h2>
|
||||||
|
<div class="mt-4 flex gap-2">
|
||||||
|
<ButtonStyled class="neon-button neon">
|
||||||
|
<button :disabled="isAuthLibPatching" @click="handleInitAuthLibPatching(true)">
|
||||||
|
Install Microsoft
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled class="neon-button neon">
|
||||||
|
<button :disabled="isAuthLibPatching" @click="handleInitAuthLibPatching(false) ">
|
||||||
|
Install Ely.By
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-if="instance.linked_data && instance.linked_data.locked">
|
<template v-if="instance.linked_data && instance.linked_data.locked">
|
||||||
@@ -787,3 +845,9 @@ const messages = defineMessages({
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-button.scss';
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-text.scss';
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-icon.scss';
|
||||||
|
</style>
|
||||||
@@ -6,9 +6,9 @@ import { edit, get_optimal_jre_key } from '@/helpers/profile'
|
|||||||
import { handleError } from '@/store/notifications'
|
import { handleError } from '@/store/notifications'
|
||||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||||
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
import JavaSelector from '@/components/ui/JavaSelector.vue'
|
||||||
import { get_max_memory } from '@/helpers/jre'
|
|
||||||
import { get } from '@/helpers/settings.ts'
|
import { get } from '@/helpers/settings.ts'
|
||||||
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
|
import type { InstanceSettingsTabProps, AppSettings, MemorySettings } from '../../../helpers/types'
|
||||||
|
import useMemorySlider from '@/composables/useMemorySlider'
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ const envVars = ref(
|
|||||||
|
|
||||||
const overrideMemorySettings = ref(!!props.instance.memory)
|
const overrideMemorySettings = ref(!!props.instance.memory)
|
||||||
const memory = ref(props.instance.memory ?? globalSettings.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 editProfileObject = computed(() => {
|
||||||
const editProfile: {
|
const editProfile: {
|
||||||
@@ -156,6 +156,8 @@ const messages = defineMessages({
|
|||||||
:min="512"
|
:min="512"
|
||||||
:max="maxMemory"
|
:max="maxMemory"
|
||||||
:step="64"
|
:step="64"
|
||||||
|
:snap-points="snapPoints"
|
||||||
|
:snap-range="512"
|
||||||
unit="MB"
|
unit="MB"
|
||||||
/>
|
/>
|
||||||
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
<h2 id="project-name" class="mt-4 mb-1 text-lg font-extrabold text-contrast block">
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
PaintbrushIcon,
|
PaintbrushIcon,
|
||||||
GameIcon,
|
GameIcon,
|
||||||
CoffeeIcon,
|
CoffeeIcon,
|
||||||
|
DownloadIcon,
|
||||||
|
SpinnerIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import { TabbedModal } from '@modrinth/ui'
|
import { TabbedModal } from '@modrinth/ui'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
@@ -23,6 +25,23 @@ import { useTheming } from '@/store/state'
|
|||||||
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
|
||||||
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
|
// [AR] Imports
|
||||||
|
import { installState, getRemote, updateState } from '@/helpers/update.js'
|
||||||
|
|
||||||
|
const updateModalView = ref(null)
|
||||||
|
const updateRequestFailView = ref(null)
|
||||||
|
|
||||||
|
const initUpdateModal = async () => {
|
||||||
|
updateModalView.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const initDownload = async () => {
|
||||||
|
updateModalView.value.hide()
|
||||||
|
const result = await getRemote(true);
|
||||||
|
if (!result) {
|
||||||
|
updateRequestFailView.value.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
|
|
||||||
@@ -138,11 +157,10 @@ function devModeCount() {
|
|||||||
{{ formatMessage(developerModeEnabled) }}
|
{{ formatMessage(developerModeEnabled) }}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
|
||||||
:class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }"
|
:class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }"
|
||||||
@click="devModeCount"
|
@click="devModeCount">
|
||||||
>
|
|
||||||
<AstralRinthLogo class="w-6 h-6" />
|
<AstralRinthLogo class="w-6 h-6" />
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
@@ -153,9 +171,80 @@ function devModeCount() {
|
|||||||
{{ osVersion }}
|
{{ osVersion }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="updateState" class="w-8 h-8 cursor-pointer hover:brightness-75 neon-icon pulse">
|
||||||
|
<template v-if="installState">
|
||||||
|
<SpinnerIcon class="size-6 animate-spin" v-tooltip.bottom="'Installing in process...'" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<DownloadIcon class="size-6" v-tooltip.bottom="'View update info'" @click="!installState && (initUpdateModal(), getRemote(false))" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</TabbedModal>
|
</TabbedModal>
|
||||||
|
<!-- [AR] Feature -->
|
||||||
|
<ModalWrapper ref="updateModalView" :has-to-type="false" header="Request to update the AstralRinth launcher">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p>The new version of the AstralRinth launcher is available.</p>
|
||||||
|
<p>Your version is outdated. We recommend that you update to the latest version.</p>
|
||||||
|
<p><strong>⚠️ Warning ⚠️</strong></p>
|
||||||
|
<p>
|
||||||
|
Before updating, make sure that you have saved all running instances and made a backup copy of the instances
|
||||||
|
that are valuable to you. Remember that the authors of the product are not responsible for the breakdown of
|
||||||
|
your files, so you should always make copies of them and keep them in a safe place.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-secondary space-y-1">
|
||||||
|
<a class="neon-text" href="https://me.astralium.su/get/ar" target="_blank"
|
||||||
|
rel="noopener noreferrer"><strong>Source:</strong> Git Astralium</a>
|
||||||
|
<p>
|
||||||
|
<strong>Version on remote server:</strong>
|
||||||
|
<span id="releaseData" class="neon-text"></span>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Version on local device:</strong>
|
||||||
|
<span class="neon-text">v{{ version }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-4 right-4 flex items-center gap-4 neon-button neon">
|
||||||
|
<Button class="bordered" @click="updateModalView.hide()">Cancel</Button>
|
||||||
|
<Button class="bordered" @click="initDownload()">Download file</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
|
<ModalWrapper ref="updateRequestFailView" :has-to-type="false" header="Failed to request a file from the server :(">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<p><strong>Error occurred</strong></p>
|
||||||
|
<p>Unfortunately, the program was unable to download the file from our servers.</p>
|
||||||
|
<p>
|
||||||
|
Please try downloading it yourself from
|
||||||
|
<a class="neon-text" href="https://me.astralium.su/get/ar" target="_blank" rel="noopener noreferrer">Git
|
||||||
|
Astralium</a>
|
||||||
|
if there are any updates available.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-secondary">
|
||||||
|
<p>
|
||||||
|
<strong>Local AstralRinth:</strong>
|
||||||
|
<span class="neon-text">v{{ version }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-4 right-4 flex items-center gap-4 neon-button neon">
|
||||||
|
<Button class="bordered" @click="updateRequestFailView.hide()">Close</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalWrapper>
|
||||||
</ModalWrapper>
|
</ModalWrapper>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-icon.scss';
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-button.scss';
|
||||||
|
@import '../../../../../../packages/assets/styles/neon-text.scss';
|
||||||
|
</style>
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { get, set } from '@/helpers/settings.ts'
|
import { get, set } from '@/helpers/settings.ts'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { get_max_memory } from '@/helpers/jre'
|
|
||||||
import { handleError } from '@/store/notifications'
|
|
||||||
import { Slider, Toggle } from '@modrinth/ui'
|
import { Slider, Toggle } from '@modrinth/ui'
|
||||||
|
import useMemorySlider from '@/composables/useMemorySlider'
|
||||||
|
|
||||||
const fetchSettings = await get()
|
const fetchSettings = await get()
|
||||||
fetchSettings.launchArgs = fetchSettings.extra_launch_args.join(' ')
|
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 settings = ref(fetchSettings)
|
||||||
|
|
||||||
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
|
const { maxMemory, snapPoints } = await useMemorySlider()
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
settings,
|
settings,
|
||||||
@@ -107,6 +106,8 @@ watch(
|
|||||||
:min="512"
|
:min="512"
|
||||||
:max="maxMemory"
|
:max="maxMemory"
|
||||||
:step="64"
|
:step="64"
|
||||||
|
:snap-points="snapPoints"
|
||||||
|
:snap-range="512"
|
||||||
unit="MB"
|
unit="MB"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ watch(
|
|||||||
option, you opt out and ads will no longer be shown based on your interests.
|
option, you opt out and ads will no longer be shown based on your interests.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- AstralRinth disabled element by default -->
|
<!-- [AR] Patch. Disabled element by default -->
|
||||||
<Toggle id="personalized-ads" v-model="settings.personalized_ads" :disabled="!settings.personalized_ads" />
|
<Toggle id="personalized-ads" v-model="settings.personalized_ads" :disabled="!settings.personalized_ads" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ watch(
|
|||||||
longer be collected.
|
longer be collected.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<!-- AstralRinth disabled element by default -->
|
<!-- [AR] Patch. Disabled element by default -->
|
||||||
<Toggle id="opt-out-analytics" v-model="settings.telemetry" :disabled="!settings.telemetry" />
|
<Toggle id="opt-out-analytics" v-model="settings.telemetry" :disabled="!settings.telemetry" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ import {
|
|||||||
type Cape,
|
type Cape,
|
||||||
type SkinModel,
|
type SkinModel,
|
||||||
get_normalized_skin_texture,
|
get_normalized_skin_texture,
|
||||||
|
determineModelType,
|
||||||
} from '@/helpers/skins.ts'
|
} from '@/helpers/skins.ts'
|
||||||
import { handleError } from '@/store/notifications'
|
import { handleError } from '@/store/notifications'
|
||||||
import {
|
import {
|
||||||
@@ -253,7 +254,7 @@ async function showNew(e: MouseEvent, skinTextureUrl: string) {
|
|||||||
mode.value = 'new'
|
mode.value = 'new'
|
||||||
currentSkin.value = null
|
currentSkin.value = null
|
||||||
uploadedTextureUrl.value = skinTextureUrl
|
uploadedTextureUrl.value = skinTextureUrl
|
||||||
variant.value = 'CLASSIC'
|
variant.value = await determineModelType(skinTextureUrl)
|
||||||
selectedCape.value = undefined
|
selectedCape.value = undefined
|
||||||
visibleCapeList.value = []
|
visibleCapeList.value = []
|
||||||
initVisibleCapeList()
|
initVisibleCapeList()
|
||||||
|
|||||||
@@ -128,6 +128,14 @@ const messages = defineMessages({
|
|||||||
id: 'instance.worlds.game_already_open',
|
id: 'instance.worlds.game_already_open',
|
||||||
defaultMessage: 'Instance is 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: {
|
copyAddress: {
|
||||||
id: 'instance.worlds.copy_address',
|
id: 'instance.worlds.copy_address',
|
||||||
defaultMessage: 'Copy address',
|
defaultMessage: 'Copy address',
|
||||||
@@ -302,39 +310,33 @@ const messages = defineMessages({
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
<div class="flex gap-1 justify-end smart-clickable:allow-pointer-events">
|
||||||
<template v-if="world.type === 'singleplayer' || serverStatus">
|
<ButtonStyled
|
||||||
<ButtonStyled
|
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
||||||
v-if="(playingWorld || (locked && playingInstance)) && !startingInstance"
|
color="red"
|
||||||
color="red"
|
>
|
||||||
>
|
<button @click="emit('stop')">
|
||||||
<button @click="emit('stop')">
|
<StopCircleIcon aria-hidden="true" />
|
||||||
<StopCircleIcon aria-hidden="true" />
|
{{ formatMessage(commonMessages.stopButton) }}
|
||||||
{{ formatMessage(commonMessages.stopButton) }}
|
</button>
|
||||||
</button>
|
</ButtonStyled>
|
||||||
</ButtonStyled>
|
<ButtonStyled v-else>
|
||||||
<ButtonStyled v-else>
|
<button
|
||||||
<button
|
v-tooltip="
|
||||||
v-tooltip="
|
!serverStatus
|
||||||
serverIncompatible
|
? formatMessage(messages.noContact)
|
||||||
? 'Server is incompatible'
|
: serverIncompatible
|
||||||
|
? formatMessage(messages.incompatibleServer)
|
||||||
: !supportsQuickPlay
|
: !supportsQuickPlay
|
||||||
? formatMessage(messages.noQuickPlay)
|
? formatMessage(messages.noQuickPlay)
|
||||||
: playingOtherWorld || locked
|
: playingOtherWorld || locked
|
||||||
? formatMessage(messages.gameAlreadyOpen)
|
? formatMessage(messages.gameAlreadyOpen)
|
||||||
: null
|
: null
|
||||||
"
|
"
|
||||||
:disabled="!supportsQuickPlay || playingOtherWorld || startingInstance"
|
:disabled="!supportsQuickPlay || playingOtherWorld || startingInstance"
|
||||||
@click="emit('play')"
|
@click="emit('play')"
|
||||||
>
|
>
|
||||||
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
<SpinnerIcon v-if="startingInstance && playingWorld" class="animate-spin" />
|
||||||
<PlayIcon v-else aria-hidden="true" />
|
<PlayIcon v-else aria-hidden="true" />
|
||||||
{{ formatMessage(commonMessages.playButton) }}
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</template>
|
|
||||||
<ButtonStyled v-else>
|
|
||||||
<button class="invisible">
|
|
||||||
<PlayIcon aria-hidden="true" />
|
|
||||||
{{ formatMessage(commonMessages.playButton) }}
|
{{ formatMessage(commonMessages.playButton) }}
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
|
|||||||
21
apps/app-frontend/src/composables/useMemorySlider.js
Normal file
21
apps/app-frontend/src/composables/useMemorySlider.js
Normal file
@@ -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 }
|
||||||
|
}
|
||||||
@@ -17,6 +17,24 @@ export async function offline_login(name) {
|
|||||||
return await invoke('plugin:auth|offline_login', { name: name })
|
return await invoke('plugin:auth|offline_login', { name: name })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
export async function elyby_login(uuid, login, accessToken) {
|
||||||
|
return await invoke('plugin:auth|elyby_login', {
|
||||||
|
uuid,
|
||||||
|
login,
|
||||||
|
accessToken
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] • Feature
|
||||||
|
export async function elyby_auth_authenticate(login, password, clientToken) {
|
||||||
|
return await invoke('plugin:auth|elyby_auth_authenticate', {
|
||||||
|
login,
|
||||||
|
password,
|
||||||
|
clientToken,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate a user with Hydra - part 1.
|
* Authenticate a user with Hydra - part 1.
|
||||||
* This begins the authentication flow quasi-synchronously.
|
* This begins the authentication flow quasi-synchronously.
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ export async function get_jre(path) {
|
|||||||
|
|
||||||
// Tests JRE version by running 'java -version' on it.
|
// Tests JRE version by running 'java -version' on it.
|
||||||
// Returns true if the version is valid, and matches given (after extraction)
|
// Returns true if the version is valid, and matches given (after extraction)
|
||||||
export async function test_jre(path, majorVersion, minorVersion) {
|
export async function test_jre(path, majorVersion) {
|
||||||
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion, minorVersion })
|
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically installs specified java version
|
// Automatically installs specified java version
|
||||||
|
|||||||
@@ -2,25 +2,46 @@ import * as THREE from 'three'
|
|||||||
import type { Skin, Cape } from '../skins'
|
import type { Skin, Cape } from '../skins'
|
||||||
import { get_normalized_skin_texture, determineModelType } from '../skins'
|
import { get_normalized_skin_texture, determineModelType } from '../skins'
|
||||||
import { reactive } from 'vue'
|
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 { skinPreviewStorage } from '../storage/skin-preview-storage'
|
||||||
import { CapeModel, ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
import { headStorage } from '../storage/head-storage'
|
||||||
|
import { ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
||||||
|
|
||||||
export interface RenderResult {
|
export interface RenderResult {
|
||||||
forwards: string
|
forwards: string
|
||||||
backwards: string
|
backwards: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RawRenderResult {
|
||||||
|
forwards: Blob
|
||||||
|
backwards: Blob
|
||||||
|
}
|
||||||
|
|
||||||
class BatchSkinRenderer {
|
class BatchSkinRenderer {
|
||||||
private renderer: THREE.WebGLRenderer
|
private renderer: THREE.WebGLRenderer | null = null
|
||||||
private readonly scene: THREE.Scene
|
private scene: THREE.Scene | null = null
|
||||||
private readonly camera: THREE.PerspectiveCamera
|
private camera: THREE.PerspectiveCamera | null = null
|
||||||
private currentModel: THREE.Group | null = null
|
private currentModel: THREE.Group | null = null
|
||||||
|
private readonly width: number
|
||||||
|
private readonly height: number
|
||||||
|
|
||||||
constructor(width: number = 360, height: number = 504) {
|
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')
|
const canvas = document.createElement('canvas')
|
||||||
canvas.width = width
|
canvas.width = this.width
|
||||||
canvas.height = height
|
canvas.height = this.height
|
||||||
|
|
||||||
this.renderer = new THREE.WebGLRenderer({
|
this.renderer = new THREE.WebGLRenderer({
|
||||||
canvas: canvas,
|
canvas: canvas,
|
||||||
@@ -33,10 +54,10 @@ class BatchSkinRenderer {
|
|||||||
this.renderer.toneMapping = THREE.NoToneMapping
|
this.renderer.toneMapping = THREE.NoToneMapping
|
||||||
this.renderer.toneMappingExposure = 10.0
|
this.renderer.toneMappingExposure = 10.0
|
||||||
this.renderer.setClearColor(0x000000, 0)
|
this.renderer.setClearColor(0x000000, 0)
|
||||||
this.renderer.setSize(width, height)
|
this.renderer.setSize(this.width, this.height)
|
||||||
|
|
||||||
this.scene = new THREE.Scene()
|
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 ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
||||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||||
@@ -50,9 +71,12 @@ class BatchSkinRenderer {
|
|||||||
textureUrl: string,
|
textureUrl: string,
|
||||||
modelUrl: string,
|
modelUrl: string,
|
||||||
capeUrl?: string,
|
capeUrl?: string,
|
||||||
capeModelUrl?: string,
|
): Promise<RawRenderResult> {
|
||||||
): Promise<RenderResult> {
|
this.initializeRenderer()
|
||||||
await this.setupModel(modelUrl, textureUrl, capeModelUrl, capeUrl)
|
|
||||||
|
this.clearScene()
|
||||||
|
|
||||||
|
await this.setupModel(modelUrl, textureUrl, capeUrl)
|
||||||
|
|
||||||
const headPart = this.currentModel!.getObjectByName('Head')
|
const headPart = this.currentModel!.getObjectByName('Head')
|
||||||
let lookAtTarget: [number, number, number]
|
let lookAtTarget: [number, number, number]
|
||||||
@@ -77,35 +101,35 @@ class BatchSkinRenderer {
|
|||||||
private async renderView(
|
private async renderView(
|
||||||
cameraPosition: [number, number, number],
|
cameraPosition: [number, number, number],
|
||||||
lookAtPosition: [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.position.set(...cameraPosition)
|
||||||
this.camera.lookAt(...lookAtPosition)
|
this.camera.lookAt(...lookAtPosition)
|
||||||
|
|
||||||
this.renderer.render(this.scene, this.camera)
|
this.renderer.render(this.scene, this.camera)
|
||||||
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
const dataUrl = this.renderer.domElement.toDataURL('image/webp', 0.9)
|
||||||
this.renderer.domElement.toBlob((blob) => {
|
const response = await fetch(dataUrl)
|
||||||
if (blob) {
|
return await response.blob()
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
resolve(url)
|
|
||||||
} else {
|
|
||||||
reject(new Error('Failed to create blob from canvas'))
|
|
||||||
}
|
|
||||||
}, 'image/png')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setupModel(
|
private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
|
||||||
modelUrl: string,
|
if (!this.scene) {
|
||||||
textureUrl: string,
|
throw new Error('Renderer not initialized')
|
||||||
capeModelUrl?: string,
|
|
||||||
capeUrl?: string,
|
|
||||||
): Promise<void> {
|
|
||||||
if (this.currentModel) {
|
|
||||||
this.scene.remove(this.currentModel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
const group = new THREE.Group()
|
||||||
group.add(model)
|
group.add(model)
|
||||||
@@ -116,8 +140,39 @@ class BatchSkinRenderer {
|
|||||||
this.currentModel = group
|
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 {
|
public dispose(): void {
|
||||||
this.renderer.dispose()
|
if (this.renderer) {
|
||||||
|
this.renderer.dispose()
|
||||||
|
}
|
||||||
disposeCaches()
|
disposeCaches()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,10 +188,25 @@ function getModelUrlForVariant(variant: string): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const map = reactive(new Map<string, RenderResult>())
|
export const skinBlobUrlMap = reactive(new Map<string, RenderResult>())
|
||||||
export const headMap = reactive(new Map<string, string>())
|
export const headBlobUrlMap = reactive(new Map<string, string>())
|
||||||
const DEBUG_MODE = false
|
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> {
|
export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
||||||
const validKeys = new Set<string>()
|
const validKeys = new Set<string>()
|
||||||
const validHeadKeys = new Set<string>()
|
const validHeadKeys = new Set<string>()
|
||||||
@@ -150,7 +220,7 @@ export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await skinPreviewStorage.cleanupInvalidKeys(validKeys)
|
await skinPreviewStorage.cleanupInvalidKeys(validKeys)
|
||||||
await skinPreviewStorage.cleanupInvalidKeys(validHeadKeys)
|
await headStorage.cleanupInvalidKeys(validHeadKeys)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to cleanup unused skin previews:', error)
|
console.warn('Failed to cleanup unused skin previews:', error)
|
||||||
}
|
}
|
||||||
@@ -229,13 +299,17 @@ export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64)
|
|||||||
outputCtx.drawImage(hatCanvas, 0, 0, 8, 8, 0, 0, size, size)
|
outputCtx.drawImage(hatCanvas, 0, 0, 8, 8, 0, 0, size, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
outputCanvas.toBlob((blob) => {
|
outputCanvas.toBlob(
|
||||||
if (blob) {
|
(blob) => {
|
||||||
resolve(blob)
|
if (blob) {
|
||||||
} else {
|
resolve(blob)
|
||||||
reject(new Error('Failed to create blob from canvas'))
|
} else {
|
||||||
}
|
reject(new Error('Failed to create blob from canvas'))
|
||||||
}, 'image/png')
|
}
|
||||||
|
},
|
||||||
|
'image/webp',
|
||||||
|
0.9,
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
reject(error)
|
reject(error)
|
||||||
}
|
}
|
||||||
@@ -252,35 +326,24 @@ export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64)
|
|||||||
async function generateHeadRender(skin: Skin): Promise<string> {
|
async function generateHeadRender(skin: Skin): Promise<string> {
|
||||||
const headKey = `${skin.texture_key}-head`
|
const headKey = `${skin.texture_key}-head`
|
||||||
|
|
||||||
if (headMap.has(headKey)) {
|
if (headBlobUrlMap.has(headKey)) {
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
const url = headMap.get(headKey)!
|
const url = headBlobUrlMap.get(headKey)!
|
||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
headMap.delete(headKey)
|
headBlobUrlMap.delete(headKey)
|
||||||
} else {
|
} 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 skinUrl = await get_normalized_skin_texture(skin)
|
||||||
const headBlob = await generatePlayerHeadBlob(skinUrl, 64)
|
const headBlob = await generatePlayerHeadBlob(skinUrl, 64)
|
||||||
const headUrl = URL.createObjectURL(headBlob)
|
const headUrl = URL.createObjectURL(headBlob)
|
||||||
|
|
||||||
headMap.set(headKey, headUrl)
|
headBlobUrlMap.set(headKey, headUrl)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// @ts-expect-error - skinPreviewStorage.store expects a RenderResult, but we are storing a string url.
|
await headStorage.store(headKey, headBlob)
|
||||||
await skinPreviewStorage.store(headKey, headUrl)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to store head render in persistent storage:', error)
|
console.warn('Failed to store head render in persistent storage:', error)
|
||||||
}
|
}
|
||||||
@@ -293,30 +356,49 @@ export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
|
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
|
||||||
const renderer = new BatchSkinRenderer()
|
|
||||||
|
|
||||||
try {
|
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) {
|
for (const skin of skins) {
|
||||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||||
|
|
||||||
if (map.has(key)) {
|
if (skinBlobUrlMap.has(key)) {
|
||||||
if (DEBUG_MODE) {
|
if (DEBUG_MODE) {
|
||||||
const result = map.get(key)!
|
const result = skinBlobUrlMap.get(key)!
|
||||||
URL.revokeObjectURL(result.forwards)
|
URL.revokeObjectURL(result.forwards)
|
||||||
URL.revokeObjectURL(result.backwards)
|
URL.revokeObjectURL(result.backwards)
|
||||||
map.delete(key)
|
skinBlobUrlMap.delete(key)
|
||||||
} else continue
|
} else continue
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const renderer = getSharedRenderer()
|
||||||
const cached = await skinPreviewStorage.retrieve(key)
|
|
||||||
if (cached) {
|
|
||||||
map.set(key, cached)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to retrieve cached skin preview:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
let variant = skin.variant
|
let variant = skin.variant
|
||||||
if (variant === 'UNKNOWN') {
|
if (variant === 'UNKNOWN') {
|
||||||
@@ -330,25 +412,35 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
|
|||||||
|
|
||||||
const modelUrl = getModelUrlForVariant(variant)
|
const modelUrl = getModelUrlForVariant(variant)
|
||||||
const cape: Cape | undefined = capes.find((_cape) => _cape.id === skin.cape_id)
|
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),
|
await get_normalized_skin_texture(skin),
|
||||||
modelUrl,
|
modelUrl,
|
||||||
cape?.texture,
|
cape?.texture,
|
||||||
CapeModel,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
map.set(key, renderResult)
|
const renderResult: RenderResult = {
|
||||||
|
forwards: URL.createObjectURL(rawRenderResult.forwards),
|
||||||
|
backwards: URL.createObjectURL(rawRenderResult.backwards),
|
||||||
|
}
|
||||||
|
|
||||||
|
skinBlobUrlMap.set(key, renderResult)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await skinPreviewStorage.store(key, renderResult)
|
await skinPreviewStorage.store(key, rawRenderResult)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to store skin preview in persistent storage:', 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 {
|
} finally {
|
||||||
renderer.dispose()
|
disposeSharedRenderer()
|
||||||
await cleanupUnusedPreviews(skins)
|
await cleanupUnusedPreviews(skins)
|
||||||
|
|
||||||
|
await skinPreviewStorage.debugCalculateStorage()
|
||||||
|
await headStorage.debugCalculateStorage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,15 +62,12 @@ export async function determineModelType(texture: string): Promise<'SLIM' | 'CLA
|
|||||||
|
|
||||||
context.drawImage(image, 0, 0)
|
context.drawImage(image, 0, 0)
|
||||||
|
|
||||||
const armX = 44
|
const armX = 54
|
||||||
const armY = 16
|
const armY = 20
|
||||||
const armWidth = 4
|
const armWidth = 2
|
||||||
const armHeight = 12
|
const armHeight = 12
|
||||||
|
|
||||||
const imageData = context.getImageData(armX, armY, armWidth, armHeight).data
|
const imageData = context.getImageData(armX, armY, armWidth, armHeight).data
|
||||||
|
for (let alphaIndex = 3; alphaIndex < imageData.length; alphaIndex += 4) {
|
||||||
for (let y = 0; y < armHeight; y++) {
|
|
||||||
const alphaIndex = (3 + y * armWidth) * 4 + 3
|
|
||||||
if (imageData[alphaIndex] !== 0) {
|
if (imageData[alphaIndex] !== 0) {
|
||||||
resolve('CLASSIC')
|
resolve('CLASSIC')
|
||||||
return
|
return
|
||||||
|
|||||||
229
apps/app-frontend/src/helpers/storage/head-storage.ts
Normal file
229
apps/app-frontend/src/helpers/storage/head-storage.ts
Normal file
@@ -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 {
|
interface StoredPreview {
|
||||||
forwards: Blob
|
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()
|
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 transaction = this.db!.transaction(['previews'], 'readwrite')
|
||||||
const store = transaction.objectStore('previews')
|
const store = transaction.objectStore('previews')
|
||||||
|
|
||||||
const storedPreview: StoredPreview = {
|
const storedPreview: StoredPreview = {
|
||||||
forwards: forwardsBlob,
|
forwards: result.forwards,
|
||||||
backwards: backwardsBlob,
|
backwards: result.backwards,
|
||||||
timestamp: Date.now(),
|
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()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||||
@@ -70,14 +67,56 @@ export class SkinPreviewStorage {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const forwards = URL.createObjectURL(result.forwards)
|
resolve({ forwards: result.forwards, backwards: result.backwards })
|
||||||
const backwards = URL.createObjectURL(result.backwards)
|
|
||||||
resolve({ forwards, backwards })
|
|
||||||
}
|
}
|
||||||
request.onerror = () => reject(request.error)
|
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> {
|
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
||||||
if (!this.db) await this.init()
|
if (!this.db) await this.init()
|
||||||
|
|
||||||
@@ -113,6 +152,67 @@ export class SkinPreviewStorage {
|
|||||||
request.onerror = () => reject(request.error)
|
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()
|
export const skinPreviewStorage = new SkinPreviewStorage()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { getVersion } from '@tauri-apps/api/app'
|
import { getVersion } from '@tauri-apps/api/app'
|
||||||
import { getArtifact, getOS } from '@/helpers/utils.js'
|
import { initUpdateLauncher, getOS } from '@/helpers/utils.js'
|
||||||
|
|
||||||
export const allowState = ref(false)
|
export const allowState = ref(false)
|
||||||
export const installState = ref(false)
|
export const installState = ref(false)
|
||||||
@@ -11,7 +11,7 @@ const releaseLink = `https://git.astralium.su/api/v1/repos/didirus/AstralRinth/r
|
|||||||
const failedFetch = [`Failed to fetch remote releases:`, `Failed to fetch remote commits:`]
|
const failedFetch = [`Failed to fetch remote releases:`, `Failed to fetch remote commits:`]
|
||||||
|
|
||||||
const osList = ['macos', 'windows', 'linux']
|
const osList = ['macos', 'windows', 'linux']
|
||||||
const macExtensionList = ['.app', '.dmg']
|
const macExtensionList = ['.dmg', '.pkg']
|
||||||
const windowsExtensionList = ['.exe', '.msi']
|
const windowsExtensionList = ['.exe', '.msi']
|
||||||
|
|
||||||
const blacklistPrefixes = [
|
const blacklistPrefixes = [
|
||||||
@@ -52,7 +52,7 @@ export async function getRemote(isDownloadState) {
|
|||||||
installState.value = true;
|
installState.value = true;
|
||||||
const builds = remoteData.assets;
|
const builds = remoteData.assets;
|
||||||
const fileName = getInstaller(getExtension(), builds);
|
const fileName = getInstaller(getExtension(), builds);
|
||||||
result = fileName ? await getArtifact(fileName[1], fileName[0], currentOS.value, true) : false;
|
result = fileName ? await initUpdateLauncher(fileName[1], fileName[0], currentOS.value, true) : false;
|
||||||
installState.value = false;
|
installState.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,20 @@ export async function getOS() {
|
|||||||
return await invoke('plugin:utils|get_os')
|
return await invoke('plugin:utils|get_os')
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getArtifact(downloadurl, filename, ostype, autoupdatesupported) {
|
// [AR] Feature. Updater
|
||||||
console.log('Downloading build', downloadurl, filename, ostype, autoupdatesupported)
|
export async function initUpdateLauncher(downloadUrl, filename, osType, autoUpdateSupported) {
|
||||||
return await invoke('plugin:utils|get_artifact', { downloadurl, filename, ostype, autoupdatesupported })
|
console.log('Downloading build', downloadUrl, filename, osType, autoUpdateSupported)
|
||||||
|
return await invoke('plugin:utils|init_update_launcher', { downloadUrl, filename, osType, autoUpdateSupported })
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] Migration. Patch
|
||||||
|
export async function applyMigrationFix(eol) {
|
||||||
|
return await invoke('plugin:utils|apply_migration_fix', { eol })
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] Feature. Ely.by
|
||||||
|
export async function initAuthlibPatching(minecraftVersion, isMojang) {
|
||||||
|
return await invoke('plugin:utils|init_authlib_patching', { minecraftVersion, isMojang })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openPath(path) {
|
export async function openPath(path) {
|
||||||
|
|||||||
@@ -377,6 +377,12 @@
|
|||||||
"instance.worlds.hardcore": {
|
"instance.worlds.hardcore": {
|
||||||
"message": "Hardcore mode"
|
"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": {
|
"instance.worlds.no_quick_play": {
|
||||||
"message": "You can only jump straight into worlds on Minecraft 1.20+"
|
"message": "You can only jump straight into worlds on Minecraft 1.20+"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ async function refreshSearch() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.value = rawResults.result
|
results.value = rawResults.result
|
||||||
currentPage.value = Math.max(1, Math.min(pageCount.value, currentPage.value))
|
currentPage.value = 1
|
||||||
|
|
||||||
const persistentParams: LocationQuery = {}
|
const persistentParams: LocationQuery = {}
|
||||||
|
|
||||||
@@ -266,6 +266,7 @@ async function onSearchChangeToTop() {
|
|||||||
|
|
||||||
function clearSearch() {
|
function clearSearch() {
|
||||||
query.value = ''
|
query.value = ''
|
||||||
|
currentPage.value = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import {
|
|||||||
import { get as getSettings } from '@/helpers/settings.ts'
|
import { get as getSettings } from '@/helpers/settings.ts'
|
||||||
import { get_default_user, login as login_flow, users } from '@/helpers/auth'
|
import { get_default_user, login as login_flow, users } from '@/helpers/auth'
|
||||||
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
|
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 { handleSevereError } from '@/store/error'
|
||||||
import { trackEvent } from '@/helpers/analytics'
|
import { trackEvent } from '@/helpers/analytics'
|
||||||
import type AccountsCard from '@/components/ui/AccountsCard.vue'
|
import type AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||||
@@ -215,7 +215,7 @@ async function loadCurrentUser() {
|
|||||||
|
|
||||||
function getBakedSkinTextures(skin: Skin): RenderResult | undefined {
|
function getBakedSkinTextures(skin: Skin): RenderResult | undefined {
|
||||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||||
return map.get(key)
|
return skinBlobUrlMap.get(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
height: calc(100vh - 11rem);
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-row {
|
.button-row {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ fn main() {
|
|||||||
InlinedPlugin::new()
|
InlinedPlugin::new()
|
||||||
.commands(&[
|
.commands(&[
|
||||||
"offline_login",
|
"offline_login",
|
||||||
|
"elyby_login",
|
||||||
|
"elyby_auth_authenticate",
|
||||||
"login",
|
"login",
|
||||||
"remove_user",
|
"remove_user",
|
||||||
"get_default_user",
|
"get_default_user",
|
||||||
@@ -218,7 +220,9 @@ fn main() {
|
|||||||
"utils",
|
"utils",
|
||||||
InlinedPlugin::new()
|
InlinedPlugin::new()
|
||||||
.commands(&[
|
.commands(&[
|
||||||
"get_artifact",
|
"init_authlib_patching",
|
||||||
|
"apply_migration_fix",
|
||||||
|
"init_update_launcher",
|
||||||
"get_os",
|
"get_os",
|
||||||
"should_disable_mouseover",
|
"should_disable_mouseover",
|
||||||
"highlight_in_folder",
|
"highlight_in_folder",
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ use crate::api::Result;
|
|||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use tauri::plugin::TauriPlugin;
|
use tauri::plugin::TauriPlugin;
|
||||||
use tauri::{Manager, Runtime, UserAttentionType};
|
use tauri::{Manager, Runtime, UserAttentionType};
|
||||||
|
use tauri_plugin_http::reqwest::Client;
|
||||||
use theseus::prelude::*;
|
use theseus::prelude::*;
|
||||||
|
|
||||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||||
tauri::plugin::Builder::<R>::new("auth")
|
tauri::plugin::Builder::<R>::new("auth")
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
offline_login,
|
offline_login,
|
||||||
|
elyby_login,
|
||||||
|
elyby_auth_authenticate,
|
||||||
login,
|
login,
|
||||||
remove_user,
|
remove_user,
|
||||||
get_default_user,
|
get_default_user,
|
||||||
@@ -17,14 +20,65 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### AR • Feature
|
||||||
/// Create new offline user
|
/// Create new offline user
|
||||||
/// This is custom function from Astralium Org.
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn offline_login(name: &str) -> Result<Credentials> {
|
pub async fn offline_login(name: &str) -> Result<Credentials> {
|
||||||
let credentials = minecraft_auth::offline_auth(name).await?;
|
let credentials = minecraft_auth::offline_auth(name).await?;
|
||||||
Ok(credentials)
|
Ok(credentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### AR • Feature
|
||||||
|
/// Create new Ely.by user
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn elyby_login(
|
||||||
|
uuid: uuid::Uuid,
|
||||||
|
login: &str,
|
||||||
|
access_token: &str
|
||||||
|
) -> Result<Credentials> {
|
||||||
|
let credentials = minecraft_auth::elyby_auth(uuid, login, access_token).await?;
|
||||||
|
Ok(credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### AR • Feature
|
||||||
|
/// Authenticate Ely.by user
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn elyby_auth_authenticate(
|
||||||
|
login: &str,
|
||||||
|
password: &str,
|
||||||
|
client_token: &str,
|
||||||
|
) -> Result<String> {
|
||||||
|
let client = Client::new();
|
||||||
|
let auth_body = serde_json::json!({
|
||||||
|
"username": login,
|
||||||
|
"password": password,
|
||||||
|
"clientToken": client_token,
|
||||||
|
});
|
||||||
|
|
||||||
|
let response = match client
|
||||||
|
.post("https://authserver.ely.by/auth/authenticate")
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.json(&auth_body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(resp) => resp,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("[AR] • Failed to send request: {}", e);
|
||||||
|
return Ok("".to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = match response.text().await {
|
||||||
|
Ok(body) => body,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("[AR] • Failed to read response text: {}", e);
|
||||||
|
return Ok("".to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
|
||||||
/// Authenticate a user with Hydra - part 1
|
/// Authenticate a user with Hydra - part 1
|
||||||
/// This begins the authentication flow quasi-synchronously, returning a URL to visit (that the user will sign in at)
|
/// This begins the authentication flow quasi-synchronously, returning a URL to visit (that the user will sign in at)
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -10,12 +10,15 @@ use crate::api::{Result, TheseusSerializableError};
|
|||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use theseus::prelude::canonicalize;
|
use theseus::prelude::canonicalize;
|
||||||
|
use theseus::util::utils;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
||||||
tauri::plugin::Builder::new("utils")
|
tauri::plugin::Builder::new("utils")
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
get_artifact,
|
init_authlib_patching,
|
||||||
|
apply_migration_fix,
|
||||||
|
init_update_launcher,
|
||||||
get_os,
|
get_os,
|
||||||
should_disable_mouseover,
|
should_disable_mouseover,
|
||||||
highlight_in_folder,
|
highlight_in_folder,
|
||||||
@@ -27,9 +30,39 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
|
|||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// [AR] Feature. Ely.by
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_artifact(downloadurl: &str, filename: &str, ostype: &str, autoupdatesupported: bool) -> Result<()> {
|
pub async fn init_authlib_patching(
|
||||||
theseus::download::init_download(downloadurl, filename, ostype, autoupdatesupported).await;
|
minecraft_version: &str,
|
||||||
|
is_mojang: bool,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let result =
|
||||||
|
utils::init_authlib_patching(minecraft_version, is_mojang).await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [AR] Migration. Patch
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn apply_migration_fix(eol: &str) -> Result<bool> {
|
||||||
|
let result = utils::apply_migration_fix(eol).await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [AR] Feature. Updater
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn init_update_launcher(
|
||||||
|
download_url: &str,
|
||||||
|
filename: &str,
|
||||||
|
os_type: &str,
|
||||||
|
auto_update_supported: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
let _ = utils::init_update_launcher(
|
||||||
|
download_url,
|
||||||
|
filename,
|
||||||
|
os_type,
|
||||||
|
auto_update_supported,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ fn main() {
|
|||||||
*/
|
*/
|
||||||
let _log_guard = theseus::start_logger();
|
let _log_guard = theseus::start_logger();
|
||||||
|
|
||||||
tracing::info!("Initialized tracing subscriber. Loading Modrinth App!");
|
tracing::info!("Initialized tracing subscriber. Loading AstralRinth App!");
|
||||||
|
|
||||||
let mut builder = tauri::Builder::default();
|
let mut builder = tauri::Builder::default();
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"productName": "AstralRinth App",
|
"productName": "AstralRinth App",
|
||||||
"version": "0.10.302",
|
"version": "0.10.305",
|
||||||
"mainBinaryName": "AstralRinth App",
|
"mainBinaryName": "AstralRinth App",
|
||||||
"identifier": "AstralRinthApp",
|
"identifier": "AstralRinthApp",
|
||||||
"plugins": {
|
"plugins": {
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
"capabilities": ["core", "plugins"],
|
"capabilities": ["core", "plugins"],
|
||||||
"csp": {
|
"csp": {
|
||||||
"default-src": "'self' customprotocol: asset:",
|
"default-src": "'self' customprotocol: asset:",
|
||||||
"connect-src": "ipc: https://git.astralium.su http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs 'self' data: blob:",
|
"connect-src": "ipc: https://git.astralium.su https://authserver.ely.by http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs 'self' data: blob:",
|
||||||
"font-src": ["https://cdn-raw.modrinth.com/fonts/"],
|
"font-src": ["https://cdn-raw.modrinth.com/fonts/"],
|
||||||
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:",
|
"img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:",
|
||||||
"style-src": "'unsafe-inline' 'self'",
|
"style-src": "'unsafe-inline' 'self'",
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
FROM rust:1.88.0 AS build
|
FROM rust:1.88.0 AS build
|
||||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
|
||||||
|
|
||||||
WORKDIR /usr/src/daedalus
|
WORKDIR /usr/src/daedalus
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -10,11 +9,8 @@ FROM debian:bookworm-slim
|
|||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends ca-certificates openssl \
|
&& apt-get install -y --no-install-recommends ca-certificates openssl \
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN update-ca-certificates
|
|
||||||
|
|
||||||
COPY --from=build /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
|
COPY --from=build /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
|
||||||
WORKDIR /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
|
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.
|
To enable labrinth to create a project, you need to add two things.
|
||||||
|
|
||||||
1. An entry in the `loaders` table.
|
1. An entry in the `loaders` table.
|
||||||
|
|||||||
@@ -38,9 +38,10 @@
|
|||||||
"@intercom/messenger-js-sdk": "^0.0.14",
|
"@intercom/messenger-js-sdk": "^0.0.14",
|
||||||
"@ltd/j-toml": "^1.38.0",
|
"@ltd/j-toml": "^1.38.0",
|
||||||
"@modrinth/assets": "workspace:*",
|
"@modrinth/assets": "workspace:*",
|
||||||
|
"@modrinth/blog": "workspace:*",
|
||||||
|
"@modrinth/moderation": "workspace:*",
|
||||||
"@modrinth/ui": "workspace:*",
|
"@modrinth/ui": "workspace:*",
|
||||||
"@modrinth/utils": "workspace:*",
|
"@modrinth/utils": "workspace:*",
|
||||||
"@modrinth/blog": "workspace:*",
|
|
||||||
"@pinia/nuxt": "^0.5.1",
|
"@pinia/nuxt": "^0.5.1",
|
||||||
"@types/three": "^0.172.0",
|
"@types/three": "^0.172.0",
|
||||||
"@vintl/vintl": "^4.4.1",
|
"@vintl/vintl": "^4.4.1",
|
||||||
@@ -58,6 +59,7 @@
|
|||||||
"markdown-it": "14.1.0",
|
"markdown-it": "14.1.0",
|
||||||
"pathe": "^1.1.2",
|
"pathe": "^1.1.2",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
"qrcode.vue": "^3.4.0",
|
"qrcode.vue": "^3.4.0",
|
||||||
"semver": "^7.5.4",
|
"semver": "^7.5.4",
|
||||||
"three": "^0.172.0",
|
"three": "^0.172.0",
|
||||||
|
|||||||
@@ -197,13 +197,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> :where(
|
> :where(
|
||||||
input + *,
|
input + *,
|
||||||
.input-group + *,
|
.input-group + *,
|
||||||
.textarea-wrapper + *,
|
.textarea-wrapper + *,
|
||||||
.chips + *,
|
.chips + *,
|
||||||
.resizable-textarea-wrapper + *,
|
.resizable-textarea-wrapper + *,
|
||||||
.input-div + *
|
.input-div + *
|
||||||
) {
|
) {
|
||||||
&:not(:empty) {
|
&:not(:empty) {
|
||||||
margin-block-start: var(--spacing-card-md);
|
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-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-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),
|
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);
|
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;
|
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;
|
--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.35) 0%,
|
||||||
rgba(255, 255, 255, 0.2695) 100%
|
rgba(255, 255, 255, 0.2695) 100%
|
||||||
);
|
);
|
||||||
--landing-blob-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16),
|
--landing-blob-shadow:
|
||||||
inset 2px 2px 64px rgba(255, 255, 255, 0.45);
|
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-bg: rgba(255, 255, 255, 0.8);
|
||||||
--landing-card-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16);
|
--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-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
||||||
--shadow-raised: 0px -2px 4px hsla(221, 39%, 11%, 0.1);
|
--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;
|
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;
|
--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-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");
|
url("https://cdn.modrinth.com/landing-new/landing-lower.webp");
|
||||||
--landing-maze-outer-bg: linear-gradient(180deg, #06060d 0%, #000000 100%);
|
--landing-maze-outer-bg: linear-gradient(180deg, #06060d 0%, #000000 100%);
|
||||||
|
|
||||||
@@ -284,7 +288,8 @@ html {
|
|||||||
rgba(44, 48, 79, 0.35) 0%,
|
rgba(44, 48, 79, 0.35) 0%,
|
||||||
rgba(32, 35, 50, 0.2695) 100%
|
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-bg: rgba(59, 63, 85, 0.15);
|
||||||
--landing-card-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16);
|
--landing-card-shadow: 2px 2px 12px rgba(0, 0, 0, 0.16);
|
||||||
@@ -360,8 +365,9 @@ body {
|
|||||||
// Defaults
|
// Defaults
|
||||||
background-color: var(--color-bg);
|
background-color: var(--color-bg);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
--font-standard: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto,
|
--font-standard:
|
||||||
Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
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;
|
--mono-font: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
|
||||||
font-family: var(--font-standard);
|
font-family: var(--font-standard);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="vue-notification-group experimental-styles-within"
|
class="vue-notification-group experimental-styles-within"
|
||||||
:class="{ 'intercom-present': isIntercomPresent }"
|
:class="{
|
||||||
|
'intercom-present': isIntercomPresent,
|
||||||
|
rightwards: moveNotificationsRight,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<transition-group name="notifs">
|
<transition-group name="notifs">
|
||||||
<div
|
<div
|
||||||
@@ -82,6 +85,7 @@ import {
|
|||||||
CopyIcon,
|
CopyIcon,
|
||||||
} from "@modrinth/assets";
|
} from "@modrinth/assets";
|
||||||
const notifications = useNotifications();
|
const notifications = useNotifications();
|
||||||
|
const { isVisible: moveNotificationsRight } = useNotificationRightwards();
|
||||||
|
|
||||||
const isIntercomPresent = ref(false);
|
const isIntercomPresent = ref(false);
|
||||||
|
|
||||||
@@ -160,6 +164,15 @@ function copyToClipboard(notif) {
|
|||||||
bottom: 5rem;
|
bottom: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.rightwards {
|
||||||
|
right: unset !important;
|
||||||
|
left: 1.5rem;
|
||||||
|
|
||||||
|
@media screen and (max-width: 500px) {
|
||||||
|
left: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.vue-notification-wrapper {
|
.vue-notification-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
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>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -172,6 +172,7 @@ const flags = useFeatureFlags();
|
|||||||
|
|
||||||
.markdown-body {
|
.markdown-body {
|
||||||
grid-area: body;
|
grid-area: body;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reporter-info {
|
.reporter-info {
|
||||||
|
|||||||
@@ -31,9 +31,9 @@
|
|||||||
class="flex cursor-pointer items-center gap-1 bg-transparent p-0"
|
class="flex cursor-pointer items-center gap-1 bg-transparent p-0"
|
||||||
@click="
|
@click="
|
||||||
versionFilter &&
|
versionFilter &&
|
||||||
(unlockFilterAccordion.isOpen
|
(unlockFilterAccordion.isOpen
|
||||||
? unlockFilterAccordion.close()
|
? unlockFilterAccordion.close()
|
||||||
: unlockFilterAccordion.open())
|
: unlockFilterAccordion.open())
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<TagItem
|
<TagItem
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export class ModrinthServer {
|
|||||||
try {
|
try {
|
||||||
const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, {
|
const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, {
|
||||||
override: auth,
|
override: auth,
|
||||||
retry: false,
|
retry: 1, // Reduce retries for optional resources
|
||||||
});
|
});
|
||||||
|
|
||||||
if (fileData instanceof Blob && import.meta.client) {
|
if (fileData instanceof Blob && import.meta.client) {
|
||||||
@@ -124,8 +124,14 @@ export class ModrinthServer {
|
|||||||
return dataURL;
|
return dataURL;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ModrinthServerError && error.statusCode === 404) {
|
if (error instanceof ModrinthServerError) {
|
||||||
if (iconUrl) {
|
if (error.statusCode && error.statusCode >= 500) {
|
||||||
|
console.debug("Service unavailable, skipping icon processing");
|
||||||
|
sharedImage.value = undefined;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.statusCode === 404 && iconUrl) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(iconUrl);
|
const response = await fetch(iconUrl);
|
||||||
if (!response.ok) throw new Error("Failed to fetch icon");
|
if (!response.ok) throw new Error("Failed to fetch icon");
|
||||||
@@ -187,6 +193,44 @@ export class ModrinthServer {
|
|||||||
return undefined;
|
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(
|
async refresh(
|
||||||
modules: ModuleName[] = [],
|
modules: ModuleName[] = [],
|
||||||
options?: {
|
options?: {
|
||||||
@@ -200,6 +244,8 @@ export class ModrinthServer {
|
|||||||
: (["general", "content", "backups", "network", "startup", "ws", "fs"] as ModuleName[]);
|
: (["general", "content", "backups", "network", "startup", "ws", "fs"] as ModuleName[]);
|
||||||
|
|
||||||
for (const module of modulesToRefresh) {
|
for (const module of modulesToRefresh) {
|
||||||
|
this.errors[module] = undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (module) {
|
switch (module) {
|
||||||
case "general": {
|
case "general": {
|
||||||
@@ -250,7 +296,7 @@ export class ModrinthServer {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error.statusCode === 503) {
|
if (error.statusCode && error.statusCode >= 500) {
|
||||||
console.debug(`Temporary ${module} unavailable:`, error.message);
|
console.debug(`Temporary ${module} unavailable:`, error.message);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,26 +22,49 @@ export class FSModule extends ServerModule {
|
|||||||
this.opsQueuedForModification = [];
|
this.opsQueuedForModification = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async retryWithAuth<T>(requestFn: () => Promise<T>): Promise<T> {
|
private async retryWithAuth<T>(
|
||||||
|
requestFn: () => Promise<T>,
|
||||||
|
ignoreFailure: boolean = false,
|
||||||
|
): Promise<T> {
|
||||||
try {
|
try {
|
||||||
return await requestFn();
|
return await requestFn();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ModrinthServerError && error.statusCode === 401) {
|
if (error instanceof ModrinthServerError && error.statusCode === 401) {
|
||||||
|
console.debug("Auth failed, refreshing JWT and retrying");
|
||||||
await this.fetch(); // Refresh auth
|
await this.fetch(); // Refresh auth
|
||||||
return await requestFn();
|
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;
|
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 () => {
|
return this.retryWithAuth(async () => {
|
||||||
const encodedPath = encodeURIComponent(path);
|
const encodedPath = encodeURIComponent(path);
|
||||||
return await useServersFetch(`/list?path=${encodedPath}&page=${page}&page_size=${pageSize}`, {
|
return await useServersFetch(`/list?path=${encodedPath}&page=${page}&page_size=${pageSize}`, {
|
||||||
override: this.auth,
|
override: this.auth,
|
||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
});
|
}, ignoreFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
createFileOrFolder(path: string, type: "file" | "directory"): Promise<void> {
|
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 () => {
|
return this.retryWithAuth(async () => {
|
||||||
const encodedPath = encodeURIComponent(path);
|
const encodedPath = encodeURIComponent(path);
|
||||||
const fileData = await useServersFetch(`/download?path=${encodedPath}`, {
|
const fileData = await useServersFetch(`/download?path=${encodedPath}`, {
|
||||||
@@ -161,7 +184,7 @@ export class FSModule extends ServerModule {
|
|||||||
return raw ? fileData : await fileData.text();
|
return raw ? fileData : await fileData.text();
|
||||||
}
|
}
|
||||||
return fileData;
|
return fileData;
|
||||||
});
|
}, ignoreFailure);
|
||||||
}
|
}
|
||||||
|
|
||||||
extractFile(
|
extractFile(
|
||||||
|
|||||||
@@ -46,13 +46,18 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
|
|||||||
data.image = (await this.server.processImage(data.project?.icon_url)) ?? undefined;
|
data.image = (await this.server.processImage(data.project?.icon_url)) ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const motd = await this.getMotd();
|
try {
|
||||||
if (motd === "A Minecraft Server") {
|
const motd = await this.getMotd();
|
||||||
await this.setMotd(
|
if (motd === "A Minecraft Server") {
|
||||||
`§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`,
|
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
|
// Copy data to this module
|
||||||
Object.assign(this, data);
|
Object.assign(this, data);
|
||||||
@@ -178,7 +183,7 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
|
|||||||
|
|
||||||
async getMotd(): Promise<string | undefined> {
|
async getMotd(): Promise<string | undefined> {
|
||||||
try {
|
try {
|
||||||
const props = await this.server.fs.downloadFile("/server.properties");
|
const props = await this.server.fs.downloadFile("/server.properties", false, true);
|
||||||
if (props) {
|
if (props) {
|
||||||
const lines = props.split("\n");
|
const lines = props.split("\n");
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
|
|||||||
@@ -42,6 +42,23 @@ export async function useServersFetch<T>(
|
|||||||
retry = method === "GET" ? 3 : 0,
|
retry = method === "GET" ? 3 : 0,
|
||||||
} = options;
|
} = 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(
|
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> = {
|
const headers: Record<string, string> = {
|
||||||
"User-Agent": "Modrinth/1.0 (https://modrinth.com)",
|
"User-Agent": "Modrinth/1.0 (https://modrinth.com)",
|
||||||
|
"X-Archon-Request": "true",
|
||||||
Vary: "Accept, Origin",
|
Vary: "Accept, Origin",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -94,10 +112,12 @@ export async function useServersFetch<T>(
|
|||||||
const response = await $fetch<T>(fullUrl, {
|
const response = await $fetch<T>(fullUrl, {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
body: body && contentType === "application/json" ? JSON.stringify(body) : body ?? undefined,
|
body:
|
||||||
|
body && contentType === "application/json" ? JSON.stringify(body) : (body ?? undefined),
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
failureCount.value = 0;
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
lastError = error as Error;
|
lastError = error as Error;
|
||||||
@@ -107,6 +127,11 @@ export async function useServersFetch<T>(
|
|||||||
const statusCode = error.response?.status;
|
const statusCode = error.response?.status;
|
||||||
const statusText = error.response?.statusText || "Unknown error";
|
const statusText = error.response?.statusText || "Unknown error";
|
||||||
|
|
||||||
|
if (statusCode && statusCode >= 500) {
|
||||||
|
failureCount.value++;
|
||||||
|
lastFailureTime.value = now;
|
||||||
|
}
|
||||||
|
|
||||||
let v1Error: V1ErrorInfo | undefined;
|
let v1Error: V1ErrorInfo | undefined;
|
||||||
if (error.data?.error && error.data?.description) {
|
if (error.data?.error && error.data?.description) {
|
||||||
v1Error = {
|
v1Error = {
|
||||||
@@ -134,9 +159,11 @@ export async function useServersFetch<T>(
|
|||||||
? errorMessages[statusCode]
|
? errorMessages[statusCode]
|
||||||
: `HTTP Error: ${statusCode || "unknown"} ${statusText}`;
|
: `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);
|
console.error("Fetch error:", error);
|
||||||
|
|
||||||
const fetchError = new ModrinthServersFetchError(
|
const fetchError = new ModrinthServersFetchError(
|
||||||
@@ -147,7 +174,8 @@ export async function useServersFetch<T>(
|
|||||||
throw new ModrinthServerError(error.message, statusCode, fetchError, module, v1Error);
|
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})`);
|
console.warn(`Retrying request in ${delay}ms (attempt ${attempts}/${maxAttempts - 1})`);
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
12
apps/frontend/src/composables/util.ts
Normal file
12
apps/frontend/src/composables/util.ts
Normal file
@@ -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,
|
PackageOpenIcon,
|
||||||
DiscordIcon,
|
DiscordIcon,
|
||||||
BlueskyIcon,
|
BlueskyIcon,
|
||||||
TumblrIcon,
|
|
||||||
TwitterIcon,
|
TwitterIcon,
|
||||||
MastodonIcon,
|
MastodonIcon,
|
||||||
GithubIcon,
|
GithubIcon,
|
||||||
@@ -1185,13 +1184,6 @@ const socialLinks = [
|
|||||||
icon: MastodonIcon,
|
icon: MastodonIcon,
|
||||||
rel: "me",
|
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" })),
|
label: formatMessage(defineMessage({ id: "layout.footer.social.x", defaultMessage: "X" })),
|
||||||
href: "https://x.com/modrinth",
|
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": {
|
"layout.footer.about": {
|
||||||
"message": "About"
|
"message": "About"
|
||||||
},
|
},
|
||||||
"layout.footer.about.news": {
|
|
||||||
"message": "News"
|
|
||||||
},
|
|
||||||
"layout.footer.about.careers": {
|
"layout.footer.about.careers": {
|
||||||
"message": "Careers"
|
"message": "Careers"
|
||||||
},
|
},
|
||||||
"layout.footer.about.changelog": {
|
"layout.footer.about.changelog": {
|
||||||
"message": "Changelog"
|
"message": "Changelog"
|
||||||
},
|
},
|
||||||
|
"layout.footer.about.news": {
|
||||||
|
"message": "News"
|
||||||
|
},
|
||||||
"layout.footer.about.rewards-program": {
|
"layout.footer.about.rewards-program": {
|
||||||
"message": "Rewards Program"
|
"message": "Rewards Program"
|
||||||
},
|
},
|
||||||
@@ -404,6 +404,9 @@
|
|||||||
"layout.footer.legal-disclaimer": {
|
"layout.footer.legal-disclaimer": {
|
||||||
"message": "NOT AN OFFICIAL MINECRAFT SERVICE. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT."
|
"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": {
|
"layout.footer.legal.privacy-policy": {
|
||||||
"message": "Privacy Policy"
|
"message": "Privacy Policy"
|
||||||
},
|
},
|
||||||
@@ -458,9 +461,6 @@
|
|||||||
"layout.footer.social.mastodon": {
|
"layout.footer.social.mastodon": {
|
||||||
"message": "Mastodon"
|
"message": "Mastodon"
|
||||||
},
|
},
|
||||||
"layout.footer.social.tumblr": {
|
|
||||||
"message": "Tumblr"
|
|
||||||
},
|
|
||||||
"layout.footer.social.x": {
|
"layout.footer.social.x": {
|
||||||
"message": "X"
|
"message": "X"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,12 +29,11 @@
|
|||||||
class="settings-header__icon"
|
class="settings-header__icon"
|
||||||
/>
|
/>
|
||||||
<div class="settings-header__text">
|
<div class="settings-header__text">
|
||||||
<h1 class="wrap-as-needed">
|
<h1 class="wrap-as-needed">{{ project.title }}</h1>
|
||||||
{{ project.title }}
|
|
||||||
</h1>
|
|
||||||
<ProjectStatusBadge :status="project.status" />
|
<ProjectStatusBadge :status="project.status" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Project settings</h2>
|
<h2>Project settings</h2>
|
||||||
<NavStack>
|
<NavStack>
|
||||||
<NavStackItem
|
<NavStackItem
|
||||||
@@ -111,6 +110,7 @@
|
|||||||
</NavStack>
|
</NavStack>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="normal-page__content">
|
<div class="normal-page__content">
|
||||||
<ProjectMemberHeader
|
<ProjectMemberHeader
|
||||||
v-if="currentMember"
|
v-if="currentMember"
|
||||||
@@ -145,6 +145,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="experimental-styles-within">
|
<div v-else class="experimental-styles-within">
|
||||||
<NewModal ref="settingsModal">
|
<NewModal ref="settingsModal">
|
||||||
<template #title>
|
<template #title>
|
||||||
@@ -174,9 +175,11 @@
|
|||||||
<div
|
<div
|
||||||
class="animation-ring-3 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-40"
|
class="animation-ring-3 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-40"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="animation-ring-2 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-60"
|
class="animation-ring-2 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight opacity-60"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="animation-ring-1 flex items-center justify-center rounded-full border-4 border-solid border-brand bg-brand-highlight"
|
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}`"
|
:href="`modrinth://mod/${project.slug}`"
|
||||||
@click="() => installWithApp()"
|
@click="() => installWithApp()"
|
||||||
>
|
>
|
||||||
<ModrinthIcon aria-hidden="true" />
|
<ModrinthIcon aria-hidden="true" /> Install with Modrinth App
|
||||||
Install with Modrinth App
|
|
||||||
<ExternalIcon aria-hidden="true" />
|
<ExternalIcon aria-hidden="true" />
|
||||||
</a>
|
</a>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
@@ -240,6 +242,7 @@
|
|||||||
<div class="flex h-[2px] w-full rounded-2xl bg-button-bg"></div>
|
<div class="flex h-[2px] w-full rounded-2xl bg-button-bg"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mx-auto flex w-fit flex-col gap-2">
|
<div class="mx-auto flex w-fit flex-col gap-2">
|
||||||
<ButtonStyled v-if="project.game_versions.length === 1">
|
<ButtonStyled v-if="project.game_versions.length === 1">
|
||||||
<div class="disabled button-like">
|
<div class="disabled button-like">
|
||||||
@@ -327,8 +330,7 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
{{ gameVersion }}
|
{{ gameVersion }} <CheckIcon v-if="userSelectedGameVersion === gameVersion" />
|
||||||
<CheckIcon v-if="userSelectedGameVersion === gameVersion" />
|
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</ScrollablePanel>
|
</ScrollablePanel>
|
||||||
@@ -419,7 +421,6 @@
|
|||||||
</ScrollablePanel>
|
</ScrollablePanel>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AutomaticAccordion div class="flex flex-col gap-2">
|
<AutomaticAccordion div class="flex flex-col gap-2">
|
||||||
<VersionSummary
|
<VersionSummary
|
||||||
v-if="filteredRelease"
|
v-if="filteredRelease"
|
||||||
@@ -470,10 +471,14 @@
|
|||||||
class="new-page sidebar"
|
class="new-page sidebar"
|
||||||
:class="{
|
:class="{
|
||||||
'alt-layout': cosmetics.leftContentLayout,
|
'alt-layout': cosmetics.leftContentLayout,
|
||||||
'ultimate-sidebar':
|
'checklist-open':
|
||||||
showModerationChecklist &&
|
showModerationChecklist &&
|
||||||
!collapsedModerationChecklist &&
|
!collapsedModerationChecklist &&
|
||||||
!flags.alwaysShowChecklistAsPopup,
|
!flags.alwaysShowChecklistAsPopup,
|
||||||
|
'checklist-collapsed':
|
||||||
|
showModerationChecklist &&
|
||||||
|
collapsedModerationChecklist &&
|
||||||
|
!flags.alwaysShowChecklistAsPopup,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="normal-page__header relative my-4">
|
<div class="normal-page__header relative my-4">
|
||||||
@@ -485,11 +490,11 @@
|
|||||||
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
|
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
|
||||||
>
|
>
|
||||||
<button @click="(event) => downloadModal.show(event)">
|
<button @click="(event) => downloadModal.show(event)">
|
||||||
<DownloadIcon aria-hidden="true" />
|
<DownloadIcon aria-hidden="true" /> Download
|
||||||
Download
|
|
||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="contents sm:hidden">
|
<div class="contents sm:hidden">
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
size="large"
|
size="large"
|
||||||
@@ -554,9 +559,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="m-0 text-wrap text-sm font-medium leading-tight text-secondary">
|
<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!
|
Modrinth Servers is the easiest way to play with your friends without hassle!
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="m-0 text-wrap text-sm font-bold text-primary">
|
<p class="m-0 text-wrap text-sm font-bold text-primary">
|
||||||
Starting at $5<span class="text-xs"> / month</span>
|
Starting at $5<span class="text-xs"> / month</span>
|
||||||
</p>
|
</p>
|
||||||
@@ -621,6 +628,7 @@
|
|||||||
{{ option.name }}
|
{{ option.name }}
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="menu-text">
|
<div v-else class="menu-text">
|
||||||
<p class="popout-text">No collections found.</p>
|
<p class="popout-text">No collections found.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -628,8 +636,7 @@
|
|||||||
class="btn collection-button"
|
class="btn collection-button"
|
||||||
@click="(event) => $refs.modal_collection.show(event)"
|
@click="(event) => $refs.modal_collection.show(event)"
|
||||||
>
|
>
|
||||||
<PlusIcon aria-hidden="true" />
|
<PlusIcon aria-hidden="true" /> Create new collection
|
||||||
Create new collection
|
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</PopoutMenu>
|
</PopoutMenu>
|
||||||
@@ -712,25 +719,14 @@
|
|||||||
:dropdown-id="`${baseId}-more-options`"
|
:dropdown-id="`${baseId}-more-options`"
|
||||||
>
|
>
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
<template #analytics>
|
<template #analytics> <ChartIcon aria-hidden="true" /> Analytics </template>
|
||||||
<ChartIcon aria-hidden="true" />
|
|
||||||
Analytics
|
|
||||||
</template>
|
|
||||||
<template #moderation-checklist>
|
<template #moderation-checklist>
|
||||||
<ScaleIcon aria-hidden="true" />
|
<ScaleIcon aria-hidden="true" /> Review project
|
||||||
Review project
|
|
||||||
</template>
|
|
||||||
<template #report>
|
|
||||||
<ReportIcon aria-hidden="true" />
|
|
||||||
Report
|
|
||||||
</template>
|
|
||||||
<template #copy-id>
|
|
||||||
<ClipboardCopyIcon aria-hidden="true" />
|
|
||||||
Copy ID
|
|
||||||
</template>
|
</template>
|
||||||
|
<template #report> <ReportIcon aria-hidden="true" /> Report </template>
|
||||||
|
<template #copy-id> <ClipboardCopyIcon aria-hidden="true" /> Copy ID </template>
|
||||||
<template #copy-permalink>
|
<template #copy-permalink>
|
||||||
<ClipboardCopyIcon aria-hidden="true" />
|
<ClipboardCopyIcon aria-hidden="true" /> Copy permanent link
|
||||||
Copy permanent link
|
|
||||||
</template>
|
</template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
@@ -756,6 +752,7 @@
|
|||||||
updates unless the author decides to unarchive the project.
|
updates unless the author decides to unarchive the project.
|
||||||
</MessageBanner>
|
</MessageBanner>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="normal-page__sidebar">
|
<div class="normal-page__sidebar">
|
||||||
<ProjectSidebarCompatibility
|
<ProjectSidebarCompatibility
|
||||||
:project="project"
|
:project="project"
|
||||||
@@ -785,6 +782,7 @@
|
|||||||
/>
|
/>
|
||||||
<div class="card flex-card experimental-styles-within">
|
<div class="card flex-card experimental-styles-within">
|
||||||
<h2>{{ formatMessage(detailsMessages.title) }}</h2>
|
<h2>{{ formatMessage(detailsMessages.title) }}</h2>
|
||||||
|
|
||||||
<div class="details-list">
|
<div class="details-list">
|
||||||
<div class="details-list__item">
|
<div class="details-list__item">
|
||||||
<BookTextIcon aria-hidden="true" />
|
<BookTextIcon aria-hidden="true" />
|
||||||
@@ -813,53 +811,48 @@
|
|||||||
<span v-else>{{ licenseIdDisplay }}</span>
|
<span v-else>{{ licenseIdDisplay }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="project.approved"
|
v-if="project.approved"
|
||||||
v-tooltip="$dayjs(project.approved).format('MMMM D, YYYY [at] h:mm A')"
|
v-tooltip="$dayjs(project.approved).format('MMMM D, YYYY [at] h:mm A')"
|
||||||
class="details-list__item"
|
class="details-list__item"
|
||||||
>
|
>
|
||||||
<CalendarIcon aria-hidden="true" />
|
<CalendarIcon aria-hidden="true" />
|
||||||
<div>
|
<div>{{ formatMessage(detailsMessages.published, { date: publishedDate }) }}</div>
|
||||||
{{ formatMessage(detailsMessages.published, { date: publishedDate }) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
|
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
|
||||||
class="details-list__item"
|
class="details-list__item"
|
||||||
>
|
>
|
||||||
<CalendarIcon aria-hidden="true" />
|
<CalendarIcon aria-hidden="true" />
|
||||||
<div>
|
<div>{{ formatMessage(detailsMessages.created, { date: createdDate }) }}</div>
|
||||||
{{ formatMessage(detailsMessages.created, { date: createdDate }) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="project.status === 'processing' && project.queued"
|
v-if="project.status === 'processing' && project.queued"
|
||||||
v-tooltip="$dayjs(project.queued).format('MMMM D, YYYY [at] h:mm A')"
|
v-tooltip="$dayjs(project.queued).format('MMMM D, YYYY [at] h:mm A')"
|
||||||
class="details-list__item"
|
class="details-list__item"
|
||||||
>
|
>
|
||||||
<ScaleIcon aria-hidden="true" />
|
<ScaleIcon aria-hidden="true" />
|
||||||
<div>
|
<div>{{ formatMessage(detailsMessages.submitted, { date: submittedDate }) }}</div>
|
||||||
{{ formatMessage(detailsMessages.submitted, { date: submittedDate }) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="versions.length > 0 && project.updated"
|
v-if="versions.length > 0 && project.updated"
|
||||||
v-tooltip="$dayjs(project.updated).format('MMMM D, YYYY [at] h:mm A')"
|
v-tooltip="$dayjs(project.updated).format('MMMM D, YYYY [at] h:mm A')"
|
||||||
class="details-list__item"
|
class="details-list__item"
|
||||||
>
|
>
|
||||||
<VersionIcon aria-hidden="true" />
|
<VersionIcon aria-hidden="true" />
|
||||||
<div>
|
<div>{{ formatMessage(detailsMessages.updated, { date: updatedDate }) }}</div>
|
||||||
{{ formatMessage(detailsMessages.updated, { date: updatedDate }) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="normal-page__content">
|
<div class="normal-page__content">
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto"><NavTabs :links="navLinks" class="mb-4" /></div>
|
||||||
<NavTabs :links="navLinks" class="mb-4" />
|
|
||||||
</div>
|
|
||||||
<NuxtPage
|
<NuxtPage
|
||||||
v-model:project="project"
|
v-model:project="project"
|
||||||
v-model:versions="versions"
|
v-model:versions="versions"
|
||||||
@@ -877,8 +870,10 @@
|
|||||||
@delete-version="deleteVersion"
|
@delete-version="deleteVersion"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="normal-page__ultimate-sidebar">
|
<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"
|
v-if="auth.user && tags.staffRoles.includes(auth.user.role) && showModerationChecklist"
|
||||||
:project="project"
|
:project="project"
|
||||||
:future-projects="futureProjects"
|
:future-projects="futureProjects"
|
||||||
@@ -886,11 +881,25 @@
|
|||||||
:collapsed="collapsedModerationChecklist"
|
:collapsed="collapsedModerationChecklist"
|
||||||
@exit="showModerationChecklist = false"
|
@exit="showModerationChecklist = false"
|
||||||
@toggle-collapsed="collapsedModerationChecklist = !collapsedModerationChecklist"
|
@toggle-collapsed="collapsedModerationChecklist = !collapsedModerationChecklist"
|
||||||
/>
|
/> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
BookmarkIcon,
|
BookmarkIcon,
|
||||||
@@ -950,16 +959,16 @@ import {
|
|||||||
isUnderReview,
|
isUnderReview,
|
||||||
renderString,
|
renderString,
|
||||||
} from "@modrinth/utils";
|
} from "@modrinth/utils";
|
||||||
import { navigateTo } from "#app";
|
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { Tooltip } from "floating-vue";
|
import { Tooltip } from "floating-vue";
|
||||||
|
import { useLocalStorage } from "@vueuse/core";
|
||||||
|
import { navigateTo } from "#app";
|
||||||
import Accordion from "~/components/ui/Accordion.vue";
|
import Accordion from "~/components/ui/Accordion.vue";
|
||||||
// import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
// import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
|
||||||
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
|
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
|
||||||
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
import Breadcrumbs from "~/components/ui/Breadcrumbs.vue";
|
||||||
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
|
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
|
||||||
import MessageBanner from "~/components/ui/MessageBanner.vue";
|
import MessageBanner from "~/components/ui/MessageBanner.vue";
|
||||||
import ModerationChecklist from "~/components/ui/ModerationChecklist.vue";
|
|
||||||
import NavStack from "~/components/ui/NavStack.vue";
|
import NavStack from "~/components/ui/NavStack.vue";
|
||||||
import NavStackItem from "~/components/ui/NavStackItem.vue";
|
import NavStackItem from "~/components/ui/NavStackItem.vue";
|
||||||
import NavTabs from "~/components/ui/NavTabs.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 { userCollectProject } from "~/composables/user.js";
|
||||||
import { reportProject } from "~/utils/report-helpers.ts";
|
import { reportProject } from "~/utils/report-helpers.ts";
|
||||||
import { saveFeatureFlags } from "~/composables/featureFlags.ts";
|
import { saveFeatureFlags } from "~/composables/featureFlags.ts";
|
||||||
|
import NewModerationChecklist from "~/components/ui/moderation/NewModerationChecklist.vue";
|
||||||
|
|
||||||
const data = useNuxtApp();
|
const data = useNuxtApp();
|
||||||
const route = useNativeRoute();
|
const route = useNativeRoute();
|
||||||
@@ -980,6 +990,7 @@ const flags = useFeatureFlags();
|
|||||||
const cosmetics = useCosmetics();
|
const cosmetics = useCosmetics();
|
||||||
|
|
||||||
const { formatMessage } = useVIntl();
|
const { formatMessage } = useVIntl();
|
||||||
|
const { setVisible } = useNotificationRightwards();
|
||||||
|
|
||||||
const settingsModal = ref();
|
const settingsModal = ref();
|
||||||
const downloadModal = ref();
|
const downloadModal = ref();
|
||||||
@@ -1551,12 +1562,28 @@ async function copyPermalink() {
|
|||||||
|
|
||||||
const collapsedChecklist = ref(false);
|
const collapsedChecklist = ref(false);
|
||||||
|
|
||||||
const showModerationChecklist = ref(false);
|
const showModerationChecklist = useLocalStorage(
|
||||||
const collapsedModerationChecklist = ref(false);
|
`show-moderation-checklist-${project.value.id}`,
|
||||||
const futureProjects = ref([]);
|
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) {
|
if (import.meta.client && history && history.state && history.state.showChecklist) {
|
||||||
showModerationChecklist.value = true;
|
showModerationChecklist.value = true;
|
||||||
futureProjects.value = history.state.projects;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDownloadModal(event) {
|
function closeDownloadModal(event) {
|
||||||
@@ -1626,6 +1653,7 @@ const navLinks = computed(() => {
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.settings-header {
|
.settings-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1781,4 +1809,16 @@ const navLinks = computed(() => {
|
|||||||
left: 18px;
|
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>
|
</style>
|
||||||
|
|||||||
@@ -705,9 +705,9 @@ export default defineNuxtComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gallery-body {
|
.gallery-body {
|
||||||
flex-grow: 1;
|
|
||||||
width: calc(100% - 2 * var(--spacing-card-md));
|
width: calc(100% - 2 * var(--spacing-card-md));
|
||||||
padding: var(--spacing-card-sm) var(--spacing-card-md);
|
padding: var(--spacing-card-sm) var(--spacing-card-md);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
|
||||||
.gallery-info {
|
.gallery-info {
|
||||||
h2 {
|
h2 {
|
||||||
|
|||||||
@@ -1421,7 +1421,8 @@ useSeoMeta({
|
|||||||
width: 25rem;
|
width: 25rem;
|
||||||
height: 25rem;
|
height: 25rem;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
background: radial-gradient(
|
background:
|
||||||
|
radial-gradient(
|
||||||
50% 50% at 50% 50%,
|
50% 50% at 50% 50%,
|
||||||
rgba(5, 206, 69, 0.19) 0%,
|
rgba(5, 206, 69, 0.19) 0%,
|
||||||
rgba(15, 19, 49, 0.25) 100%
|
rgba(15, 19, 49, 0.25) 100%
|
||||||
|
|||||||
@@ -266,12 +266,12 @@ const getRangeOfMethod = (method) => {
|
|||||||
|
|
||||||
const maxWithdrawAmount = computed(() => {
|
const maxWithdrawAmount = computed(() => {
|
||||||
const interval = selectedMethod.value.interval;
|
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 minWithdrawAmount = computed(() => {
|
||||||
const interval = selectedMethod.value.interval;
|
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(() => {
|
const withdrawAccount = computed(() => {
|
||||||
|
|||||||
@@ -212,6 +212,10 @@ if (projects.value) {
|
|||||||
|
|
||||||
async function goToProjects() {
|
async function goToProjects() {
|
||||||
const project = projectsFiltered.value[0];
|
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({
|
await router.push({
|
||||||
name: "type-id",
|
name: "type-id",
|
||||||
params: {
|
params: {
|
||||||
@@ -220,7 +224,6 @@ async function goToProjects() {
|
|||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
showChecklist: true,
|
showChecklist: true,
|
||||||
projects: projectsFiltered.value.slice(1).map((x) => (x.slug ? x.slug : x.id)),
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ButtonStyled } from "@modrinth/ui";
|
import { Avatar, ButtonStyled } from "@modrinth/ui";
|
||||||
import { RssIcon, GitGraphIcon } from "@modrinth/assets";
|
import { RssIcon, GitGraphIcon } from "@modrinth/assets";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { articles as rawArticles } from "@modrinth/blog";
|
import { articles as rawArticles } from "@modrinth/blog";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
|
import type { User } from "@modrinth/utils";
|
||||||
import ShareArticleButtons from "~/components/ui/ShareArticleButtons.vue";
|
import ShareArticleButtons from "~/components/ui/ShareArticleButtons.vue";
|
||||||
import NewsletterButton from "~/components/ui/NewsletterButton.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(() => ({
|
const article = computed(() => ({
|
||||||
...rawArticle,
|
...rawArticle,
|
||||||
@@ -34,6 +49,8 @@ const article = computed(() => ({
|
|||||||
html,
|
html,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const authorCount = computed(() => authors?.value?.length ?? 0);
|
||||||
|
|
||||||
const articleTitle = computed(() => article.value.title);
|
const articleTitle = computed(() => article.value.title);
|
||||||
const articleUrl = computed(() => `https://modrinth.com/news/article/${route.params.slug}`);
|
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">
|
<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>
|
<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>
|
<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">
|
<div class="mt-auto flex flex-wrap items-center gap-1 text-sm text-secondary sm:text-base">
|
||||||
Posted on {{ dayjsDate.format("MMMM D, YYYY") }}
|
<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>
|
</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" />
|
<ShareArticleButtons :title="article.title" :url="articleUrl" />
|
||||||
<img
|
<img
|
||||||
:src="article.thumbnail"
|
:src="article.thumbnail"
|
||||||
|
|||||||
@@ -149,7 +149,8 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.main-hero {
|
.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);
|
var(--color-accent-contrast);
|
||||||
margin-top: -5rem;
|
margin-top: -5rem;
|
||||||
padding: 11.25rem 1rem 8rem;
|
padding: 11.25rem 1rem 8rem;
|
||||||
|
|||||||
@@ -45,8 +45,9 @@
|
|||||||
<h2
|
<h2
|
||||||
class="relative m-0 max-w-2xl text-base font-normal leading-[155%] text-secondary md:text-[1.2rem]"
|
class="relative m-0 max-w-2xl text-base font-normal leading-[155%] text-secondary md:text-[1.2rem]"
|
||||||
>
|
>
|
||||||
Modrinth Servers is the easiest way to host your own Minecraft server. Seamlessly install
|
Modrinth Servers is the easiest way to host your own Minecraft: Java Edition server.
|
||||||
and play your favorite mods and modpacks, all within the Modrinth platform.
|
Seamlessly install and play your favorite mods and modpacks, all within the Modrinth
|
||||||
|
platform.
|
||||||
</h2>
|
</h2>
|
||||||
<div class="relative flex w-full flex-wrap items-center gap-8 align-middle sm:w-fit">
|
<div class="relative flex w-full flex-wrap items-center gap-8 align-middle sm:w-fit">
|
||||||
<div
|
<div
|
||||||
@@ -427,11 +428,8 @@
|
|||||||
Do Modrinth Servers have DDoS protection?
|
Do Modrinth Servers have DDoS protection?
|
||||||
</summary>
|
</summary>
|
||||||
<p class="m-0 ml-6 leading-[160%]">
|
<p class="m-0 ml-6 leading-[160%]">
|
||||||
Yes. All Modrinth Servers come with DDoS protection powered by
|
Yes. All Modrinth Servers come with DDoS protection, with up to 17Tbps capacity in
|
||||||
<a href="https://us.ovhcloud.com/security/anti-ddos/" target="_blank"
|
some locations.
|
||||||
>OVHcloud® Anti-DDoS infrastructure</a
|
|
||||||
>
|
|
||||||
which has over 17Tbps capacity. Your server is safe on Modrinth.
|
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -443,8 +441,9 @@
|
|||||||
Where are Modrinth Servers located? Can I choose a region?
|
Where are Modrinth Servers located? Can I choose a region?
|
||||||
</summary>
|
</summary>
|
||||||
<p class="m-0 ml-6 leading-[160%]">
|
<p class="m-0 ml-6 leading-[160%]">
|
||||||
We have servers in both North America in Vint Hill, Virginia, and Europe in Limburg,
|
We have servers available in North America and Europe at the moment that you can
|
||||||
Germany. More regions to come in the future!
|
choose upon purchase. More regions to come in the future! If you'd like to switch
|
||||||
|
your region, please contact support.
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -461,7 +460,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details pyro-hash="players" class="group" :open="$route.hash === '#players'">
|
<details pyro-hash="performance" class="group" :open="$route.hash === '#performance'">
|
||||||
<summary class="flex cursor-pointer items-center py-3 font-medium text-contrast">
|
<summary class="flex cursor-pointer items-center py-3 font-medium text-contrast">
|
||||||
<span class="mr-2 transition-transform duration-200 group-open:rotate-90">
|
<span class="mr-2 transition-transform duration-200 group-open:rotate-90">
|
||||||
<RightArrowIcon />
|
<RightArrowIcon />
|
||||||
@@ -482,7 +481,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details pyro-hash="players" class="group" :open="$route.hash === '#prices'">
|
<details pyro-hash="prices" class="group" :open="$route.hash === '#prices'">
|
||||||
<summary class="flex cursor-pointer items-center py-3 font-medium text-contrast">
|
<summary class="flex cursor-pointer items-center py-3 font-medium text-contrast">
|
||||||
<span class="mr-2 transition-transform duration-200 group-open:rotate-90">
|
<span class="mr-2 transition-transform duration-200 group-open:rotate-90">
|
||||||
<RightArrowIcon />
|
<RightArrowIcon />
|
||||||
@@ -493,6 +492,24 @@
|
|||||||
All prices are listed in United States Dollars (USD).
|
All prices are listed in United States Dollars (USD).
|
||||||
</p>
|
</p>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details pyro-hash="versions" class="group" :open="$route.hash === '#versions'">
|
||||||
|
<summary class="flex cursor-pointer items-center py-3 font-medium text-contrast">
|
||||||
|
<span class="mr-2 transition-transform duration-200 group-open:rotate-90">
|
||||||
|
<RightArrowIcon />
|
||||||
|
</span>
|
||||||
|
What Minecraft versions and loaders can be used?
|
||||||
|
</summary>
|
||||||
|
<p class="m-0 ml-6 leading-[160%]">
|
||||||
|
Modrinth Servers can run any version of Minecraft: Java Edition going all the way
|
||||||
|
back to version 1.2.5, including snapshot versions.
|
||||||
|
</p>
|
||||||
|
<p class="m-0 ml-6 mt-3 leading-[160%]">
|
||||||
|
We also support a wide range of mod and plugin loaders, including Fabric, Quilt,
|
||||||
|
Forge, and NeoForge for mods, as well as Paper and Purpur for plugins. Availability
|
||||||
|
depends on whether the mod or plugin loader supports the selected Minecraft version.
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -719,31 +736,32 @@ async function fetchCapacityStatuses(customProduct = null) {
|
|||||||
product.metadata.ram < min.metadata.ram ? product : min,
|
product.metadata.ram < min.metadata.ram ? product : min,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
const capacityChecks = productsToCheck.map((product) =>
|
const capacityChecks = [];
|
||||||
useServersFetch("stock", {
|
for (const product of productsToCheck) {
|
||||||
method: "POST",
|
capacityChecks.push(
|
||||||
body: {
|
useServersFetch("stock", {
|
||||||
cpu: product.metadata.cpu,
|
method: "POST",
|
||||||
memory_mb: product.metadata.ram,
|
body: {
|
||||||
swap_mb: product.metadata.swap,
|
cpu: product.metadata.cpu,
|
||||||
storage_mb: product.metadata.storage,
|
memory_mb: product.metadata.ram,
|
||||||
},
|
swap_mb: product.metadata.swap,
|
||||||
bypassAuth: true,
|
storage_mb: product.metadata.storage,
|
||||||
}),
|
},
|
||||||
);
|
bypassAuth: true,
|
||||||
|
}),
|
||||||
const results = await Promise.all(capacityChecks);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (customProduct?.metadata) {
|
if (customProduct?.metadata) {
|
||||||
return {
|
return {
|
||||||
custom: results[0],
|
custom: await capacityChecks[0],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
small: results[0],
|
small: await capacityChecks[0],
|
||||||
medium: results[1],
|
medium: await capacityChecks[1],
|
||||||
large: results[2],
|
large: await capacityChecks[2],
|
||||||
custom: results[3],
|
custom: await capacityChecks[3],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -760,6 +778,11 @@ async function fetchCapacityStatuses(customProduct = null) {
|
|||||||
const { data: capacityStatuses, refresh: refreshCapacity } = await useAsyncData(
|
const { data: capacityStatuses, refresh: refreshCapacity } = await useAsyncData(
|
||||||
"ServerCapacityAll",
|
"ServerCapacityAll",
|
||||||
fetchCapacityStatuses,
|
fetchCapacityStatuses,
|
||||||
|
{
|
||||||
|
getCachedData() {
|
||||||
|
return null; // Dont cache stock data.
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const isSmallAtCapacity = computed(() => capacityStatuses.value?.small?.available === 0);
|
const isSmallAtCapacity = computed(() => capacityStatuses.value?.small?.available === 0);
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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"
|
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||||
>
|
>
|
||||||
<ErrorInformationCard
|
<ErrorInformationCard
|
||||||
@@ -68,22 +68,22 @@
|
|||||||
<template #description>
|
<template #description>
|
||||||
<div class="text-md space-y-4">
|
<div class="text-md space-y-4">
|
||||||
<p class="leading-[170%] text-secondary">
|
<p class="leading-[170%] text-secondary">
|
||||||
Your server's node, where your Modrinth Server is physically hosted, is experiencing
|
Your server's node, where your Modrinth Server is physically hosted, is not accessible
|
||||||
issues. We are working with our datacenter to resolve the issue as quickly as possible.
|
at the moment. We are working to resolve the issue as quickly as possible.
|
||||||
</p>
|
</p>
|
||||||
<p class="leading-[170%] text-secondary">
|
<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
|
Your data is safe and will not be lost, and your server will be back online as soon as
|
||||||
the issue is resolved.
|
the issue is resolved.
|
||||||
</p>
|
</p>
|
||||||
<p class="leading-[170%] text-secondary">
|
<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.
|
bubble in the bottom right corner and we'll be happy to help.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ErrorInformationCard>
|
</ErrorInformationCard>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<!-- <div
|
||||||
v-else-if="server.moduleErrors?.general?.error"
|
v-else-if="server.moduleErrors?.general?.error"
|
||||||
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
|
||||||
>
|
>
|
||||||
@@ -96,19 +96,14 @@
|
|||||||
>
|
>
|
||||||
<template #description>
|
<template #description>
|
||||||
<div class="space-y-4">
|
<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">
|
<p class="text-lg text-secondary">
|
||||||
Something went wrong, and we couldn't connect to your server. This is likely due to a
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ErrorInformationCard>
|
</ErrorInformationCard>
|
||||||
</div>
|
</div> -->
|
||||||
<!-- SERVER START -->
|
<!-- SERVER START -->
|
||||||
<div
|
<div
|
||||||
v-else-if="serverData"
|
v-else-if="serverData"
|
||||||
@@ -355,7 +350,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, watch, type Reactive } from "vue";
|
import { ref, computed, onMounted, onUnmounted, type Reactive } from "vue";
|
||||||
import {
|
import {
|
||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
@@ -371,15 +366,15 @@ import DOMPurify from "dompurify";
|
|||||||
import { ButtonStyled, ErrorInformationCard, ServerNotice } from "@modrinth/ui";
|
import { ButtonStyled, ErrorInformationCard, ServerNotice } from "@modrinth/ui";
|
||||||
import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
|
import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
|
||||||
import type { MessageDescriptor } from "@vintl/vintl";
|
import type { MessageDescriptor } from "@vintl/vintl";
|
||||||
import type {
|
import {
|
||||||
ServerState,
|
type ServerState,
|
||||||
Stats,
|
type Stats,
|
||||||
WSEvent,
|
type WSEvent,
|
||||||
WSInstallationResultEvent,
|
type WSInstallationResultEvent,
|
||||||
Backup,
|
type Backup,
|
||||||
PowerAction,
|
type PowerAction,
|
||||||
} from "@modrinth/utils";
|
} from "@modrinth/utils";
|
||||||
import { reloadNuxtApp, navigateTo } from "#app";
|
import { reloadNuxtApp } from "#app";
|
||||||
import { useModrinthServersConsole } from "~/store/console.ts";
|
import { useModrinthServersConsole } from "~/store/console.ts";
|
||||||
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
|
||||||
import { ModrinthServer, useModrinthServers } from "~/composables/servers/modrinth-servers.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 isReconnecting = ref(false);
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
const reconnectInterval = ref<ReturnType<typeof setInterval> | null>(null);
|
const reconnectInterval = ref<ReturnType<typeof setInterval> | null>(null);
|
||||||
const isFirstMount = ref(true);
|
|
||||||
const isMounted = ref(true);
|
const isMounted = ref(true);
|
||||||
const flags = useFeatureFlags();
|
const flags = useFeatureFlags();
|
||||||
|
|
||||||
@@ -422,18 +416,6 @@ const loadModulesPromise = Promise.resolve().then(() => {
|
|||||||
|
|
||||||
provide("modulesLoaded", loadModulesPromise);
|
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 errorTitle = ref("Error");
|
||||||
const errorMessage = ref("An unexpected error occurred.");
|
const errorMessage = ref("An unexpected error occurred.");
|
||||||
const errorLog = ref("");
|
const errorLog = ref("");
|
||||||
@@ -697,7 +679,6 @@ const startUptimeUpdates = () => {
|
|||||||
const stopUptimeUpdates = () => {
|
const stopUptimeUpdates = () => {
|
||||||
if (uptimeIntervalId) {
|
if (uptimeIntervalId) {
|
||||||
clearInterval(uptimeIntervalId);
|
clearInterval(uptimeIntervalId);
|
||||||
pollingIntervalId = null;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -836,8 +817,6 @@ const handleInstallationResult = async (data: WSInstallationResultEvent) => {
|
|||||||
case "ok": {
|
case "ok": {
|
||||||
if (!serverData.value) break;
|
if (!serverData.value) break;
|
||||||
|
|
||||||
stopPolling();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
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 = {
|
export type BackupInProgressReason = {
|
||||||
type: string;
|
type: string;
|
||||||
tooltip: MessageDescriptor;
|
tooltip: MessageDescriptor;
|
||||||
@@ -1035,54 +1006,6 @@ const backupInProgress = computed(() => {
|
|||||||
return undefined;
|
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(() => [
|
const nodeUnavailableDetails = computed(() => [
|
||||||
{
|
{
|
||||||
label: "Server ID",
|
label: "Server ID",
|
||||||
@@ -1091,9 +1014,16 @@ const nodeUnavailableDetails = computed(() => [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Node",
|
label: "Node",
|
||||||
value: server.general?.datacenter ?? "Unknown! Please contact support!",
|
value: server.general?.datacenter ?? "Unknown",
|
||||||
type: "inline" as const,
|
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(() => {
|
const suspendedDescription = computed(() => {
|
||||||
@@ -1160,16 +1090,10 @@ const generalErrorAction = computed(() => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const nodeUnavailableAction = 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",
|
label: "Reload",
|
||||||
onClick: () => reloadNuxtApp(),
|
onClick: () => reloadNuxtApp(),
|
||||||
color: "brand" as const,
|
color: "brand" as const,
|
||||||
disabled: formattedTime.value !== "00",
|
disabled: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const copyServerDebugInfo = () => {
|
const copyServerDebugInfo = () => {
|
||||||
@@ -1193,7 +1117,6 @@ const cleanup = () => {
|
|||||||
|
|
||||||
shutdown();
|
shutdown();
|
||||||
|
|
||||||
stopPolling();
|
|
||||||
stopUptimeUpdates();
|
stopUptimeUpdates();
|
||||||
if (reconnectInterval.value) {
|
if (reconnectInterval.value) {
|
||||||
clearInterval(reconnectInterval.value);
|
clearInterval(reconnectInterval.value);
|
||||||
@@ -1236,16 +1159,31 @@ async function dismissNotice(noticeId: number) {
|
|||||||
await server.refresh(["general"]);
|
await server.refresh(["general"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nodeAccessible = ref(true);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
isMounted.value = true;
|
isMounted.value = true;
|
||||||
if (server.general?.status === "suspended") {
|
if (server.general?.status === "suspended") {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
return;
|
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) {
|
||||||
if (!server.moduleErrors.general?.error?.message?.includes("Forbidden")) {
|
isLoading.value = false;
|
||||||
startPolling();
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
}
|
}
|
||||||
@@ -1297,21 +1235,6 @@ onUnmounted(() => {
|
|||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
|
||||||
() => serverData.value?.status,
|
|
||||||
(newStatus, oldStatus) => {
|
|
||||||
if (isFirstMount.value) {
|
|
||||||
isFirstMount.value = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newStatus === "installing" && oldStatus !== "installing") {
|
|
||||||
countdown.value = 15;
|
|
||||||
startPolling();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: "auth",
|
middleware: "auth",
|
||||||
});
|
});
|
||||||
@@ -1354,7 +1277,8 @@ useHead({
|
|||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
filter: blur(1rem);
|
filter: blur(1rem);
|
||||||
content: "";
|
content: "";
|
||||||
background-image: linear-gradient(
|
background-image:
|
||||||
|
linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
rgba(from var(--color-raised-bg) r g b / 0.2),
|
rgba(from var(--color-raised-bg) r g b / 0.2),
|
||||||
rgb(from var(--color-raised-bg) r g b / 0.8)
|
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 }">
|
<span :class="{ invisible: 'current_file' in op && !op.current_file }">
|
||||||
{{
|
{{
|
||||||
"current_file" in op
|
"current_file" in op
|
||||||
? op.current_file?.split("/")?.pop() ?? "unknown"
|
? (op.current_file?.split("/")?.pop() ?? "unknown")
|
||||||
: "unknown"
|
: "unknown"
|
||||||
}}
|
}}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
6
apps/frontend/src/public/.well-known/security.txt
Normal file
6
apps/frontend/src/public/.well-known/security.txt
Normal file
@@ -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/
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "A Pride Month Success: Over $8,400 Raised for The Trevor Project!",
|
"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",
|
"thumbnail": "https://modrinth.com/news/article/pride-campaign-2025/thumbnail.webp",
|
||||||
"date": "2025-07-01T18:00:00.000Z",
|
"date": "2025-07-01T18:00:00.000Z",
|
||||||
"link": "https://modrinth.com/news/article/pride-campaign-2025"
|
"link": "https://modrinth.com/news/article/pride-campaign-2025"
|
||||||
@@ -98,13 +98,6 @@
|
|||||||
"date": "2023-02-01T20:00:00.000Z",
|
"date": "2023-02-01T20:00:00.000Z",
|
||||||
"link": "https://modrinth.com/news/article/accelerating-development"
|
"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",
|
"title": "Modrinth's Anniversary Update",
|
||||||
"summary": "Marking two years of Modrinth and discussing our New Year's Resolutions for 2023.",
|
"summary": "Marking two years of Modrinth and discussing our New Year's Resolutions for 2023.",
|
||||||
@@ -112,16 +105,23 @@
|
|||||||
"date": "2023-01-07T00:00:00.000Z",
|
"date": "2023-01-07T00:00:00.000Z",
|
||||||
"link": "https://modrinth.com/news/article/two-years-of-modrinth"
|
"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!",
|
"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",
|
"thumbnail": "https://modrinth.com/news/article/creator-monetization/thumbnail.webp",
|
||||||
"date": "2022-11-12T00:00:00.000Z",
|
"date": "2022-11-12T00:00:00.000Z",
|
||||||
"link": "https://modrinth.com/news/article/creator-monetization"
|
"link": "https://modrinth.com/news/article/creator-monetization"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Modrinth's Carbon Ads experiment",
|
"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",
|
"thumbnail": "https://modrinth.com/news/article/carbon-ads/thumbnail.webp",
|
||||||
"date": "2022-09-08T00:00:00.000Z",
|
"date": "2022-09-08T00:00:00.000Z",
|
||||||
"link": "https://modrinth.com/news/article/carbon-ads"
|
"link": "https://modrinth.com/news/article/carbon-ads"
|
||||||
@@ -149,14 +149,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "This week in Modrinth development: Filters and Fixes",
|
"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",
|
"thumbnail": "https://modrinth.com/news/article/knossos-v2.1.0/thumbnail.webp",
|
||||||
"date": "2022-03-09T00:00:00.000Z",
|
"date": "2022-03-09T00:00:00.000Z",
|
||||||
"link": "https://modrinth.com/news/article/knossos-v2.1.0"
|
"link": "https://modrinth.com/news/article/knossos-v2.1.0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Now showing on Modrinth: A new look!",
|
"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",
|
"thumbnail": "https://modrinth.com/news/article/redesign/thumbnail.webp",
|
||||||
"date": "2022-02-27T00:00:00.000Z",
|
"date": "2022-02-27T00:00:00.000Z",
|
||||||
"link": "https://modrinth.com/news/article/redesign"
|
"link": "https://modrinth.com/news/article/redesign"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "\n SELECT\n id, user_id, price_id, amount, currency_code, status, due, last_attempt,\n charge_type, subscription_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n subscription_interval AS \"subscription_interval?\",\n payment_platform,\n payment_platform_id AS \"payment_platform_id?\",\n parent_charge_id AS \"parent_charge_id?\",\n net AS \"net?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled')",
|
"query": "\n SELECT\n id, user_id, price_id, amount, currency_code, status, due, last_attempt,\n charge_type, subscription_id,\n -- Workaround for https://github.com/launchbadge/sqlx/issues/3336\n subscription_interval AS \"subscription_interval?\",\n payment_platform,\n payment_platform_id AS \"payment_platform_id?\",\n parent_charge_id AS \"parent_charge_id?\",\n net AS \"net?\"\n FROM charges\n WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled' OR status = 'failed')",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -102,5 +102,5 @@
|
|||||||
true
|
true
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "99cca53fd3f35325e2da3b671532bf98b8c7ad8e7cb9158e4eb9c5bac66d20b2"
|
"hash": "cf020daa52a1316e5f60d197196b880b72c0b2a576e470d9fd7182558103d055"
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ paste.workspace = true
|
|||||||
meilisearch-sdk = { workspace = true, features = ["reqwest"] }
|
meilisearch-sdk = { workspace = true, features = ["reqwest"] }
|
||||||
rust-s3.workspace = true
|
rust-s3.workspace = true
|
||||||
reqwest = { workspace = true, features = ["http2", "rustls-tls-webpki-roots", "json", "multipart"] }
|
reqwest = { workspace = true, features = ["http2", "rustls-tls-webpki-roots", "json", "multipart"] }
|
||||||
hyper-tls.workspace = true
|
hyper-rustls.workspace = true
|
||||||
hyper-util.workspace = true
|
hyper-util.workspace = true
|
||||||
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
FROM rust:1.88.0 AS build
|
FROM rust:1.88.0 AS build
|
||||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
|
||||||
|
|
||||||
WORKDIR /usr/src/labrinth
|
WORKDIR /usr/src/labrinth
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY apps/labrinth/.sqlx/ .sqlx/
|
RUN SQLX_OFFLINE=true cargo build --release --package labrinth
|
||||||
RUN cargo build --release --package labrinth
|
|
||||||
|
|
||||||
|
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
@@ -14,12 +11,9 @@ LABEL org.opencontainers.image.description="Modrinth API"
|
|||||||
LABEL org.opencontainers.image.licenses=AGPL-3.0
|
LABEL org.opencontainers.image.licenses=AGPL-3.0
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get install -y --no-install-recommends ca-certificates openssl dumb-init \
|
&& apt-get install -y --no-install-recommends ca-certificates dumb-init curl \
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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/target/release/labrinth /labrinth/labrinth
|
||||||
COPY --from=build /usr/src/labrinth/apps/labrinth/migrations/* /labrinth/migrations/
|
COPY --from=build /usr/src/labrinth/apps/labrinth/migrations/* /labrinth/migrations/
|
||||||
COPY --from=build /usr/src/labrinth/apps/labrinth/assets /labrinth/assets
|
COPY --from=build /usr/src/labrinth/apps/labrinth/assets /labrinth/assets
|
||||||
|
|||||||
@@ -43,7 +43,9 @@ pub enum AuthenticationError {
|
|||||||
InvalidAuthMethod,
|
InvalidAuthMethod,
|
||||||
#[error("GitHub Token from incorrect Client ID")]
|
#[error("GitHub Token from incorrect Client ID")]
|
||||||
InvalidClientId,
|
InvalidClientId,
|
||||||
#[error("User email/account is already registered on Modrinth")]
|
#[error(
|
||||||
|
"User email is already registered on Modrinth. Try 'Forgot password' to access your account."
|
||||||
|
)]
|
||||||
DuplicateUser,
|
DuplicateUser,
|
||||||
#[error("Invalid state sent, you probably need to get a new websocket")]
|
#[error("Invalid state sent, you probably need to get a new websocket")]
|
||||||
SocketError,
|
SocketError,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use hyper_tls::{HttpsConnector, native_tls};
|
use hyper_rustls::HttpsConnectorBuilder;
|
||||||
use hyper_util::client::legacy::connect::HttpConnector;
|
|
||||||
use hyper_util::rt::TokioExecutor;
|
use hyper_util::rt::TokioExecutor;
|
||||||
|
|
||||||
mod fetch;
|
mod fetch;
|
||||||
@@ -15,13 +14,11 @@ pub async fn init_client_with_database(
|
|||||||
database: &str,
|
database: &str,
|
||||||
) -> clickhouse::error::Result<clickhouse::Client> {
|
) -> clickhouse::error::Result<clickhouse::Client> {
|
||||||
let client = {
|
let client = {
|
||||||
let mut http_connector = HttpConnector::new();
|
let https_connector = HttpsConnectorBuilder::new()
|
||||||
http_connector.enforce_http(false); // allow https URLs
|
.with_native_roots()?
|
||||||
|
.https_or_http()
|
||||||
let tls_connector =
|
.enable_all_versions()
|
||||||
native_tls::TlsConnector::builder().build().unwrap().into();
|
.build();
|
||||||
let https_connector =
|
|
||||||
HttpsConnector::from((http_connector, tls_connector));
|
|
||||||
let hyper_client =
|
let hyper_client =
|
||||||
hyper_util::client::legacy::Client::builder(TokioExecutor::new())
|
hyper_util::client::legacy::Client::builder(TokioExecutor::new())
|
||||||
.build(https_connector);
|
.build(https_connector);
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ impl DBCharge {
|
|||||||
) -> Result<Option<DBCharge>, DatabaseError> {
|
) -> Result<Option<DBCharge>, DatabaseError> {
|
||||||
let user_subscription_id = user_subscription_id.0;
|
let user_subscription_id = user_subscription_id.0;
|
||||||
let res = select_charges_with_predicate!(
|
let res = select_charges_with_predicate!(
|
||||||
"WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled')",
|
"WHERE subscription_id = $1 AND (status = 'open' OR status = 'cancelled' OR status = 'failed')",
|
||||||
user_subscription_id
|
user_subscription_id
|
||||||
)
|
)
|
||||||
.fetch_optional(exec)
|
.fetch_optional(exec)
|
||||||
|
|||||||
@@ -223,8 +223,8 @@ impl TempUser {
|
|||||||
stripe_customer_id: None,
|
stripe_customer_id: None,
|
||||||
totp_secret: None,
|
totp_secret: None,
|
||||||
username,
|
username,
|
||||||
email: self.email,
|
email: self.email.clone(),
|
||||||
email_verified: true,
|
email_verified: self.email.is_some(),
|
||||||
avatar_url,
|
avatar_url,
|
||||||
raw_avatar_url,
|
raw_avatar_url,
|
||||||
bio: self.bio,
|
bio: self.bio,
|
||||||
@@ -1419,15 +1419,15 @@ pub async fn create_account_with_password(
|
|||||||
.hash_password(new_account.password.as_bytes(), &salt)?
|
.hash_password(new_account.password.as_bytes(), &salt)?
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
if crate::database::models::DBUser::get_by_email(
|
if !crate::database::models::DBUser::get_by_case_insensitive_email(
|
||||||
&new_account.email,
|
&new_account.email,
|
||||||
&**pool,
|
&**pool,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.is_some()
|
.is_empty()
|
||||||
{
|
{
|
||||||
return Err(ApiError::InvalidInput(
|
return Err(ApiError::InvalidInput(
|
||||||
"Email is already registered on Modrinth!".to_string(),
|
"Email is already registered on Modrinth! Try 'Forgot password' to access your account.".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2220,6 +2220,18 @@ pub async fn set_email(
|
|||||||
.await?
|
.await?
|
||||||
.1;
|
.1;
|
||||||
|
|
||||||
|
if !crate::database::models::DBUser::get_by_case_insensitive_email(
|
||||||
|
&email.email,
|
||||||
|
&**pool,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
return Err(ApiError::InvalidInput(
|
||||||
|
"Email is already registered on Modrinth! Try 'Forgot password' in incognito to access and delete your other account.".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let mut transaction = pool.begin().await?;
|
let mut transaction = pool.begin().await?;
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
"app:build": "turbo run build --filter=@modrinth/app",
|
"app:build": "turbo run build --filter=@modrinth/app",
|
||||||
"app:fix": "turbo run fix --filter=@modrinth/app",
|
"app:fix": "turbo run fix --filter=@modrinth/app",
|
||||||
"app:intl:extract": "pnpm run --filter=@modrinth/app-frontend intl:extract",
|
"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",
|
"pages:build": "NITRO_PRESET=cloudflare-pages pnpm --filter frontend run build",
|
||||||
|
"moderation:fix": "turbo run fix --filter=@modrinth/moderation",
|
||||||
"build": "turbo run build --continue",
|
"build": "turbo run build --continue",
|
||||||
"lint": "turbo run lint --continue",
|
"lint": "turbo run lint --continue",
|
||||||
"test": "turbo run test --continue",
|
"test": "turbo run test --continue",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n SELECT\n uuid, active, username, access_token, refresh_token, expires\n FROM minecraft_users\n WHERE active = TRUE\n ",
|
"query": "\n SELECT\n uuid, active, username, access_token, refresh_token, expires, account_type\n FROM minecraft_users\n WHERE active = TRUE\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -32,6 +32,11 @@
|
|||||||
"name": "expires",
|
"name": "expires",
|
||||||
"ordinal": 5,
|
"ordinal": 5,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "account_type",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -43,8 +48,9 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "bf7d47350092d87c478009adaab131168e87bb37aa65c2156ad2cb6198426d8c"
|
"hash": "57214178fb3a0ccd8f67457e9732a706cbc4a4f5190c9320d1ad6111b9711d63"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"db_name": "SQLite",
|
"db_name": "SQLite",
|
||||||
"query": "\n SELECT\n uuid, active, username, access_token, refresh_token, expires\n FROM minecraft_users\n ",
|
"query": "\n SELECT\n uuid, active, username, access_token, refresh_token, expires, account_type\n FROM minecraft_users\n ",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
@@ -32,6 +32,11 @@
|
|||||||
"name": "expires",
|
"name": "expires",
|
||||||
"ordinal": 5,
|
"ordinal": 5,
|
||||||
"type_info": "Integer"
|
"type_info": "Integer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "account_type",
|
||||||
|
"ordinal": 6,
|
||||||
|
"type_info": "Text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
@@ -43,8 +48,9 @@
|
|||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "727e3e1bc8625bbcb833920059bb8cea926ac6c65d613904eff1d740df30acda"
|
"hash": "5c803f3d90c147210e8e7a7a6d7234d3801bc38c23e1e02fbd8fa08ae51e8f08"
|
||||||
}
|
}
|
||||||
12
packages/app-lib/.sqlx/query-8f7d4406ddae4a158eabb20fc6a8ffb21c4e22c7ff33459df5049ee441fa0467.json
generated
Normal file
12
packages/app-lib/.sqlx/query-8f7d4406ddae4a158eabb20fc6a8ffb21c4e22c7ff33459df5049ee441fa0467.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "\n INSERT INTO minecraft_users (uuid, active, username, access_token, refresh_token, expires, account_type)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT (uuid) DO UPDATE SET\n active = $2,\n username = $3,\n access_token = $4,\n refresh_token = $5,\n expires = $6,\n account_type = $7\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 7
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "8f7d4406ddae4a158eabb20fc6a8ffb21c4e22c7ff33459df5049ee441fa0467"
|
||||||
|
}
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"db_name": "SQLite",
|
|
||||||
"query": "\n INSERT INTO minecraft_users (uuid, active, username, access_token, refresh_token, expires)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (uuid) DO UPDATE SET\n active = $2,\n username = $3,\n access_token = $4,\n refresh_token = $5,\n expires = $6\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [],
|
|
||||||
"parameters": {
|
|
||||||
"Right": 6
|
|
||||||
},
|
|
||||||
"nullable": []
|
|
||||||
},
|
|
||||||
"hash": "d719cf2f6f87c5ea7ea6ace2d6a1828ee58a724f06a91633b8a40b4e04d0b9a0"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
-- [AR] - SQL Migration
|
||||||
|
ALTER TABLE minecraft_users ADD COLUMN account_type varchar(32) NOT NULL DEFAULT 'unknown';
|
||||||
|
|
||||||
|
UPDATE minecraft_users SET account_type = 'microsoft' WHERE access_token != 'null';
|
||||||
|
UPDATE minecraft_users SET account_type = 'pirate' WHERE access_token == 'null';
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
use std::process::exit;
|
|
||||||
|
|
||||||
use reqwest;
|
|
||||||
use tokio::fs::File as AsyncFile;
|
|
||||||
use tokio::io::AsyncWriteExt;
|
|
||||||
use tokio::process::Command;
|
|
||||||
|
|
||||||
async fn download_file(download_url: &str, local_filename: &str, os_type: &str, auto_update_supported: bool) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
let download_dir = dirs::download_dir().ok_or("[download_file] • Failed to determine download directory")?;
|
|
||||||
let full_path = download_dir.join(local_filename);
|
|
||||||
let response = reqwest::get(download_url).await?;
|
|
||||||
let bytes = response.bytes().await?;
|
|
||||||
let mut dest_file = AsyncFile::create(&full_path).await?;
|
|
||||||
dest_file.write_all(&bytes).await?;
|
|
||||||
println!("[download_file] • File downloaded to: {:?}", full_path);
|
|
||||||
if auto_update_supported {
|
|
||||||
let status;
|
|
||||||
if os_type.to_lowercase() == "Windows".to_lowercase() {
|
|
||||||
status = Command::new("explorer")
|
|
||||||
.arg(download_dir.display().to_string())
|
|
||||||
.status()
|
|
||||||
.await
|
|
||||||
.expect("[download_file] • Failed to open downloads folder");
|
|
||||||
} else if os_type.to_lowercase() == "MacOS".to_lowercase() {
|
|
||||||
status = Command::new("open")
|
|
||||||
.arg(full_path.to_str().unwrap_or_default())
|
|
||||||
.status()
|
|
||||||
.await
|
|
||||||
.expect("[download_file] • Failed to execute command");
|
|
||||||
} else {
|
|
||||||
status = Command::new(".")
|
|
||||||
.arg(full_path.to_str().unwrap_or_default())
|
|
||||||
.status()
|
|
||||||
.await
|
|
||||||
.expect("[download_file] • Failed to execute command");
|
|
||||||
}
|
|
||||||
if status.success() {
|
|
||||||
println!("[download_file] • File opened successfully!");
|
|
||||||
} else {
|
|
||||||
eprintln!("[download_file] • Failed to open the file. Exit code: {:?}", status.code());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init_download(download_url: &str, local_filename: &str, os_type: &str, auto_update_supported: bool) {
|
|
||||||
println!("[init_download] • Initialize downloading from • {:?}", download_url);
|
|
||||||
println!("[init_download] • Save local file name • {:?}", local_filename);
|
|
||||||
if let Err(e) = download_file(download_url, local_filename, os_type, auto_update_supported).await {
|
|
||||||
eprintln!("[init_download] • An error occurred! Failed to download the file: {}", e);
|
|
||||||
} else {
|
|
||||||
println!("[init_download] • Code finishes without errors.");
|
|
||||||
exit(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -166,10 +166,18 @@ pub async fn test_jre(
|
|||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
major_version: u32,
|
major_version: u32,
|
||||||
) -> crate::Result<bool> {
|
) -> crate::Result<bool> {
|
||||||
let Ok(jre) = jre::check_java_at_filepath(&path).await else {
|
let jre = match jre::check_java_at_filepath(&path).await {
|
||||||
return Ok(false);
|
Ok(jre) => jre,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("Invalid Java at {}: {e}", path.display());
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let version = extract_java_version(&jre.version)?;
|
let version = extract_java_version(&jre.version)?;
|
||||||
|
tracing::info!(
|
||||||
|
"Expected Java version {major_version}, and found {version} at {}",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
Ok(version == major_version)
|
Ok(version == major_version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,16 @@ pub async fn offline_auth(
|
|||||||
crate::state::offline_auth(name, &state.pool).await
|
crate::state::offline_auth(name, &state.pool).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn elyby_auth(
|
||||||
|
uuid: uuid::Uuid,
|
||||||
|
login: &str,
|
||||||
|
access_token: &str
|
||||||
|
) -> crate::Result<Credentials> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
crate::state::elyby_auth(uuid, login, access_token, &state.pool).await
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn get_default_user() -> crate::Result<Option<uuid::Uuid>> {
|
pub async fn get_default_user() -> crate::Result<Option<uuid::Uuid>> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ pub static DEFAULT_SKINS: LazyLock<Vec<Skin>> = LazyLock::new(|| {
|
|||||||
Skin {
|
Skin {
|
||||||
texture_key: Arc::from("66206c8f51d13d2d31c54696a58a3e8bcd1e5e7db9888d331d0753129324e4f1"),
|
texture_key: Arc::from("66206c8f51d13d2d31c54696a58a3e8bcd1e5e7db9888d331d0753129324e4f1"),
|
||||||
name: Some(Arc::from("Party Alex")),
|
name: Some(Arc::from("Party Alex")),
|
||||||
variant: MinecraftSkinVariant::Classic,
|
variant: MinecraftSkinVariant::Slim,
|
||||||
cape_id: None,
|
cape_id: None,
|
||||||
texture: Arc::from(Url::try_from(
|
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="
|
"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="
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ pub mod pack;
|
|||||||
pub mod process;
|
pub mod process;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
|
pub mod update; // [AR] Feature
|
||||||
pub mod tags;
|
pub mod tags;
|
||||||
pub mod download; // AstralRinth
|
|
||||||
pub mod worlds;
|
pub mod worlds;
|
||||||
|
|
||||||
pub mod data {
|
pub mod data {
|
||||||
|
|||||||
@@ -284,6 +284,12 @@ async fn import_mmc_unmanaged(
|
|||||||
component.version.clone().unwrap_or_default(),
|
component.version.clone().unwrap_or_default(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if component.uid.starts_with("net.neoforged") {
|
||||||
|
return Some((
|
||||||
|
PackDependency::NeoForge,
|
||||||
|
component.version.clone().unwrap_or_default(),
|
||||||
|
));
|
||||||
|
}
|
||||||
if component.uid.starts_with("org.quiltmc.quilt-loader") {
|
if component.uid.starts_with("org.quiltmc.quilt-loader") {
|
||||||
return Some((
|
return Some((
|
||||||
PackDependency::QuiltLoader,
|
PackDependency::QuiltLoader,
|
||||||
|
|||||||
117
packages/app-lib/src/api/update.rs
Normal file
117
packages/app-lib/src/api/update.rs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
use reqwest;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio::fs::File as AsyncFile;
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
pub(crate) async fn get_resource(
|
||||||
|
download_url: &str,
|
||||||
|
local_filename: &str,
|
||||||
|
os_type: &str,
|
||||||
|
auto_update_supported: bool,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let download_dir = dirs::download_dir()
|
||||||
|
.ok_or("[AR] • Failed to determine download directory")?;
|
||||||
|
let full_path = download_dir.join(local_filename);
|
||||||
|
|
||||||
|
let response = reqwest::get(download_url).await?;
|
||||||
|
let bytes = response.bytes().await?;
|
||||||
|
let mut dest_file = AsyncFile::create(&full_path).await?;
|
||||||
|
dest_file.write_all(&bytes).await?;
|
||||||
|
tracing::info!("[AR] • File downloaded to: {:?}", full_path);
|
||||||
|
|
||||||
|
if auto_update_supported {
|
||||||
|
let result = match os_type.to_lowercase().as_str() {
|
||||||
|
"windows" => handle_windows_file(&full_path).await,
|
||||||
|
"macos" => open_macos_file(&full_path).await,
|
||||||
|
_ => open_default(&full_path).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => tracing::info!("[AR] • File opened successfully!"),
|
||||||
|
Err(e) => tracing::info!("[AR] • Failed to open file: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_windows_file(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let filename = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|f| f.to_str())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
|
if filename.ends_with(".exe") || filename.ends_with(".msi") {
|
||||||
|
tracing::info!("[AR] • Detected installer: {}", filename);
|
||||||
|
run_windows_installer(path).await
|
||||||
|
} else {
|
||||||
|
open_windows_folder(path).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_windows_installer(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let installer_path = path.to_str().unwrap_or_default();
|
||||||
|
|
||||||
|
let status = if installer_path.ends_with(".msi") {
|
||||||
|
Command::new("msiexec")
|
||||||
|
.args(&["/i", installer_path, "/quiet"])
|
||||||
|
.status()
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
Command::new("cmd")
|
||||||
|
.args(&["/C", installer_path])
|
||||||
|
.status()
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
|
if status.success() {
|
||||||
|
tracing::info!("[AR] • Installer started successfully.");
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
tracing::error!("Installer failed. Exit code: {:?}", status.code());
|
||||||
|
tracing::info!("[AR] • Trying to open folder...");
|
||||||
|
open_windows_folder(path).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn open_windows_folder(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let folder = path.parent().unwrap_or(path);
|
||||||
|
let status = Command::new("explorer")
|
||||||
|
.arg(folder.display().to_string())
|
||||||
|
.status()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
Err(format!("Exit code: {:?}", status.code()).into())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn open_macos_file(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let status = Command::new("open")
|
||||||
|
.arg(path.to_str().unwrap_or_default())
|
||||||
|
.status()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
Err(format!("Exit code: {:?}", status.code()).into())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn open_default(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let status = Command::new(".")
|
||||||
|
.arg(path.to_str().unwrap_or_default())
|
||||||
|
.status()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
Err(format!("Exit code: {:?}", status.code()).into())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -151,6 +151,41 @@ pub enum ErrorKind {
|
|||||||
"A skin texture must have a dimension of either 64x64 or 64x32 pixels"
|
"A skin texture must have a dimension of either 64x64 or 64x32 pixels"
|
||||||
)]
|
)]
|
||||||
InvalidSkinTexture,
|
InvalidSkinTexture,
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"[AR] Target minecraft {minecraft_version} version doesn't exist."
|
||||||
|
)]
|
||||||
|
InvalidMinecraftVersion {
|
||||||
|
minecraft_version: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"[AR] Target metadata not found for minecraft version {minecraft_version}."
|
||||||
|
)]
|
||||||
|
MinecraftMetadataNotFound {
|
||||||
|
minecraft_version: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"[AR] Network error: {error}"
|
||||||
|
)]
|
||||||
|
NetworkErrorOccurred {
|
||||||
|
error: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"[AR] IO error: {error}"
|
||||||
|
)]
|
||||||
|
IOErrorOccurred {
|
||||||
|
error: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"[AR] Parse error: {reason}"
|
||||||
|
)]
|
||||||
|
ParseError {
|
||||||
|
reason: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@@ -6,15 +6,15 @@ use crate::launcher::download::download_log_config;
|
|||||||
use crate::launcher::io::IOError;
|
use crate::launcher::io::IOError;
|
||||||
use crate::profile::QuickPlayType;
|
use crate::profile::QuickPlayType;
|
||||||
use crate::state::{
|
use crate::state::{
|
||||||
Credentials, JavaVersion, ProcessMetadata, ProfileInstallStage,
|
AccountType, Credentials, JavaVersion, ProcessMetadata, ProfileInstallStage,
|
||||||
};
|
};
|
||||||
use crate::util::io;
|
use crate::util::{io, utils};
|
||||||
use crate::{State, get_resource_file, process, state as st};
|
use crate::{State, get_resource_file, process, state as st};
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use daedalus as d;
|
use daedalus as d;
|
||||||
use daedalus::minecraft::{LoggingSide, RuleAction, VersionInfo};
|
use daedalus::minecraft::{LoggingSide, RuleAction, VersionInfo};
|
||||||
use daedalus::modded::LoaderVersion;
|
use daedalus::modded::LoaderVersion;
|
||||||
use rand::seq::SliceRandom; // AstralRinth
|
use rand::seq::SliceRandom; // [AR] Feature
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use st::Profile;
|
use st::Profile;
|
||||||
@@ -633,6 +633,34 @@ pub async fn launch_minecraft(
|
|||||||
command.arg("--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED");
|
command.arg("--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [AR] Patch
|
||||||
|
if credentials.account_type == AccountType::Pirate.as_lowercase_str() {
|
||||||
|
if version_jar == "1.16.4" || version_jar == "1.16.5" {
|
||||||
|
let invalid_url = "https://invalid.invalid";
|
||||||
|
tracing::info!(
|
||||||
|
"[AR] • The launcher detected the launch of {} on the offline account. Applying offline multiplayer fixes.",
|
||||||
|
version_jar
|
||||||
|
);
|
||||||
|
command.arg("-Dminecraft.api.env=custom");
|
||||||
|
command.arg(format!("-Dminecraft.api.auth.host={}", invalid_url));
|
||||||
|
command
|
||||||
|
.arg(format!("-Dminecraft.api.account.host={}", invalid_url));
|
||||||
|
command
|
||||||
|
.arg(format!("-Dminecraft.api.session.host={}", invalid_url));
|
||||||
|
command
|
||||||
|
.arg(format!("-Dminecraft.api.services.host={}", invalid_url));
|
||||||
|
}
|
||||||
|
} else if credentials.account_type == AccountType::ElyBy.as_lowercase_str()
|
||||||
|
{
|
||||||
|
tracing::info!(
|
||||||
|
"[AR] • The launcher detected the launch of {} on the Ely.by account. Applying Ely.by Java Injector.",
|
||||||
|
version_jar
|
||||||
|
);
|
||||||
|
let path_buf = utils::get_or_download_elyby_injector().await?;
|
||||||
|
let path = path_buf.to_str().unwrap();
|
||||||
|
command.arg(format!("-javaagent:{}=ely.by", path));
|
||||||
|
}
|
||||||
|
|
||||||
command
|
command
|
||||||
.arg("com.modrinth.theseus.MinecraftLaunch")
|
.arg("com.modrinth.theseus.MinecraftLaunch")
|
||||||
.arg(version_info.main_class.clone())
|
.arg(version_info.main_class.clone())
|
||||||
@@ -730,11 +758,12 @@ pub async fn launch_minecraft(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [AR] Feature
|
||||||
let selected_phrase = ACTIVE_STATE.choose(&mut rand::thread_rng()).unwrap();
|
let selected_phrase = ACTIVE_STATE.choose(&mut rand::thread_rng()).unwrap();
|
||||||
let _ = state
|
let _ = state
|
||||||
.discord_rpc
|
.discord_rpc
|
||||||
.set_activity(&format!("{} {}", selected_phrase, profile.name), true)
|
.set_activity(&format!("{} {}", selected_phrase, profile.name), true)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let _ = state
|
let _ = state
|
||||||
.friends_socket
|
.friends_socket
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ and launching Modrinth mod packs
|
|||||||
#![deny(unused_must_use)]
|
#![deny(unused_must_use)]
|
||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod util;
|
pub mod util; // [AR] Refactor
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
mod config;
|
mod config;
|
||||||
|
|||||||
@@ -1,17 +1,32 @@
|
|||||||
|
use crate::ErrorKind;
|
||||||
use crate::state::DirectoryInfo;
|
use crate::state::DirectoryInfo;
|
||||||
use sqlx::sqlite::{
|
use sqlx::sqlite::{
|
||||||
SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions,
|
SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions,
|
||||||
};
|
};
|
||||||
use sqlx::{Pool, Sqlite};
|
use sqlx::{Pool, Sqlite};
|
||||||
use tokio::time::Instant;
|
use std::collections::HashMap;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use tokio::time::Instant;
|
||||||
|
|
||||||
pub(crate) async fn connect() -> crate::Result<Pool<Sqlite>> {
|
pub(crate) async fn connect() -> crate::Result<Pool<Sqlite>> {
|
||||||
|
let pool = connect_without_migrate().await?;
|
||||||
|
|
||||||
|
sqlx::migrate!().run(&pool).await?;
|
||||||
|
|
||||||
|
if let Err(err) = stale_data_cleanup(&pool).await {
|
||||||
|
tracing::warn!(
|
||||||
|
"Failed to clean up stale data from state database: {err}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(pool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// [AR] Feature. Implement SQLite3 connection without SQLx migrations.
|
||||||
|
async fn connect_without_migrate() -> crate::Result<Pool<Sqlite>> {
|
||||||
let settings_dir = DirectoryInfo::get_initial_settings_dir().ok_or(
|
let settings_dir = DirectoryInfo::get_initial_settings_dir().ok_or(
|
||||||
crate::ErrorKind::FSError(
|
ErrorKind::FSError("Could not find valid config dir".to_string()),
|
||||||
"Could not find valid config dir".to_string(),
|
|
||||||
),
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if !settings_dir.exists() {
|
if !settings_dir.exists() {
|
||||||
@@ -19,7 +34,6 @@ pub(crate) async fn connect() -> crate::Result<Pool<Sqlite>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let db_path = settings_dir.join("app.db");
|
let db_path = settings_dir.join("app.db");
|
||||||
let db_exists = db_path.exists();
|
|
||||||
|
|
||||||
let uri = format!("sqlite:{}", db_path.display());
|
let uri = format!("sqlite:{}", db_path.display());
|
||||||
let conn_options = SqliteConnectOptions::from_str(&uri)?
|
let conn_options = SqliteConnectOptions::from_str(&uri)?
|
||||||
@@ -33,22 +47,6 @@ pub(crate) async fn connect() -> crate::Result<Pool<Sqlite>> {
|
|||||||
.connect_with(conn_options)
|
.connect_with(conn_options)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if db_exists {
|
|
||||||
fix_modrinth_issued_migrations(&pool).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlx::migrate!().run(&pool).await?;
|
|
||||||
|
|
||||||
if !db_exists {
|
|
||||||
fix_modrinth_issued_migrations(&pool).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(err) = stale_data_cleanup(&pool).await {
|
|
||||||
tracing::warn!(
|
|
||||||
"Failed to clean up stale data from state database: {err}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(pool)
|
Ok(pool)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,62 +72,103 @@ async fn stale_data_cleanup(pool: &Pool<Sqlite>) -> crate::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
// Patch by AstralRinth - 08.07.2025
|
// [AR] Patch fix
|
||||||
Problem files:
|
Problem files, view detailed information in .gitattributes:
|
||||||
/packages/app-lib/migrations/20240711194701_init.sql !eol
|
/packages/app-lib/migrations/20240711194701_init.sql !eol
|
||||||
|
CRLF -> 4c47e326f16f2b1efca548076ce638d4c90dd610172fe48c47d6de9bc46ef1c5abeadfdea05041ddd72c3819fa10c040
|
||||||
|
LF -> e973512979feac07e415405291eefafc1ef0bd89454958ad66f5452c381db8679c20ffadab55194ecf6ba8ec4ca2db21
|
||||||
/packages/app-lib/migrations/20240813205023_drop-active-unique.sql !eol
|
/packages/app-lib/migrations/20240813205023_drop-active-unique.sql !eol
|
||||||
|
CRLF -> C8FD2EFE72E66E394732599EA8D93CE1ED337F098697B3ADAD40DD37CC6367893E199A8D7113B44A3D0FFB537692F91D
|
||||||
|
LF -> 5b53534a7ffd74eebede234222be47e1d37bd0cc5fee4475212491b0c0379c16e3079e08eee0af959b1fa20835eeb206
|
||||||
/packages/app-lib/migrations/20240930001852_disable-personalized-ads.sql !eol
|
/packages/app-lib/migrations/20240930001852_disable-personalized-ads.sql !eol
|
||||||
|
CRLF -> C0DE804F171B5530010EDAE087A6E75645C0E90177E28365F935C9FDD9A5C68E24850B8C1498E386A379D525D520BC57
|
||||||
|
LF -> c0de804f171b5530010edae087a6e75645c0e90177e28365f935c9fdd9a5c68e24850b8c1498e386a379d525d520bc57
|
||||||
/packages/app-lib/migrations/20241222013857_feature-flags.sql !eol
|
/packages/app-lib/migrations/20241222013857_feature-flags.sql !eol
|
||||||
|
CRLF -> 6B6F097E5BB45A397C96C3F1DC9C2A18433564E81DB264FE08A4775198CCEAC03C9E63C3605994ECB19C281C37D8F6AE
|
||||||
|
LF -> c17542cb989a0466153e695bfa4717f8970feee185ca186a2caa1f2f6c5d4adb990ab97c26cacfbbe09c39ac81551704
|
||||||
*/
|
*/
|
||||||
async fn fix_modrinth_issued_migrations(
|
pub(crate) async fn apply_migration_fix(eol: &str) -> crate::Result<bool> {
|
||||||
pool: &Pool<Sqlite>,
|
|
||||||
) -> crate::Result<()> {
|
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
tracing::info!("Fixing modrinth issued migrations");
|
|
||||||
sqlx::query(
|
// Create connection to the database without migrations
|
||||||
r#"
|
let pool = connect_without_migrate().await?;
|
||||||
UPDATE "_sqlx_migrations"
|
|
||||||
SET checksum = X'e973512979feac07e415405291eefafc1ef0bd89454958ad66f5452c381db8679c20ffadab55194ecf6ba8ec4ca2db21'
|
|
||||||
WHERE version = '20240711194701';
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.execute(pool)
|
|
||||||
.await?;
|
|
||||||
tracing::info!("⚙️ Fixed first migration");
|
|
||||||
sqlx::query(
|
|
||||||
r#"
|
|
||||||
UPDATE "_sqlx_migrations"
|
|
||||||
SET checksum = X'5b53534a7ffd74eebede234222be47e1d37bd0cc5fee4475212491b0c0379c16e3079e08eee0af959b1fa20835eeb206'
|
|
||||||
WHERE version = '20240813205023';
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.execute(pool)
|
|
||||||
.await?;
|
|
||||||
tracing::info!("⚙️ Fixed second migration");
|
|
||||||
sqlx::query(
|
|
||||||
r#"
|
|
||||||
UPDATE "_sqlx_migrations"
|
|
||||||
SET checksum = X'c0de804f171b5530010edae087a6e75645c0e90177e28365f935c9fdd9a5c68e24850b8c1498e386a379d525d520bc57'
|
|
||||||
WHERE version = '20240930001852';
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.execute(pool)
|
|
||||||
.await?;
|
|
||||||
tracing::info!("⚙️ Fixed third migration");
|
|
||||||
sqlx::query(
|
|
||||||
r#"
|
|
||||||
UPDATE "_sqlx_migrations"
|
|
||||||
SET checksum = X'c17542cb989a0466153e695bfa4717f8970feee185ca186a2caa1f2f6c5d4adb990ab97c26cacfbbe09c39ac81551704'
|
|
||||||
WHERE version = '20241222013857';
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.execute(pool)
|
|
||||||
.await?;
|
|
||||||
tracing::info!("⚙️ Fixed fourth migration");
|
|
||||||
let elapsed = started.elapsed();
|
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
"✅ Fixed all known modrinth-issued migrations in {:.2?}",
|
"⚙️ Patching Modrinth corrupted migration checksums using EOL standard: {eol}"
|
||||||
elapsed
|
|
||||||
);
|
);
|
||||||
Ok(())
|
|
||||||
|
// validate EOL input
|
||||||
|
if eol != "lf" && eol != "crlf" {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// [eol][version] -> checksum
|
||||||
|
let checksums: HashMap<(&str, &str), &str> = HashMap::from([
|
||||||
|
(
|
||||||
|
("lf", "20240711194701"),
|
||||||
|
"e973512979feac07e415405291eefafc1ef0bd89454958ad66f5452c381db8679c20ffadab55194ecf6ba8ec4ca2db21",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
("crlf", "20240711194701"),
|
||||||
|
"4c47e326f16f2b1efca548076ce638d4c90dd610172fe48c47d6de9bc46ef1c5abeadfdea05041ddd72c3819fa10c040",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
("lf", "20240813205023"),
|
||||||
|
"5b53534a7ffd74eebede234222be47e1d37bd0cc5fee4475212491b0c0379c16e3079e08eee0af959b1fa20835eeb206",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
("crlf", "20240813205023"),
|
||||||
|
"C8FD2EFE72E66E394732599EA8D93CE1ED337F098697B3ADAD40DD37CC6367893E199A8D7113B44A3D0FFB537692F91D",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
("lf", "20240930001852"),
|
||||||
|
"c0de804f171b5530010edae087a6e75645c0e90177e28365f935c9fdd9a5c68e24850b8c1498e386a379d525d520bc57",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
("crlf", "20240930001852"),
|
||||||
|
"C0DE804F171B5530010EDAE087A6E75645C0E90177E28365F935C9FDD9A5C68E24850B8C1498E386A379D525D520BC57",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
("lf", "20241222013857"),
|
||||||
|
"c17542cb989a0466153e695bfa4717f8970feee185ca186a2caa1f2f6c5d4adb990ab97c26cacfbbe09c39ac81551704",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
("crlf", "20241222013857"),
|
||||||
|
"6B6F097E5BB45A397C96C3F1DC9C2A18433564E81DB264FE08A4775198CCEAC03C9E63C3605994ECB19C281C37D8F6AE",
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let mut changed = false;
|
||||||
|
|
||||||
|
for ((eol_key, version), checksum) in checksums.iter() {
|
||||||
|
if *eol_key != eol {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"⏳ Patching checksum for migration {version} ({})",
|
||||||
|
eol.to_uppercase()
|
||||||
|
);
|
||||||
|
|
||||||
|
let result = sqlx::query(&format!(
|
||||||
|
r#"
|
||||||
|
UPDATE "_sqlx_migrations"
|
||||||
|
SET checksum = X'{checksum}'
|
||||||
|
WHERE version = '{version}';
|
||||||
|
"#
|
||||||
|
))
|
||||||
|
.execute(&pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if result.rows_affected() > 0 {
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"✅ Checksum patching completed in {:.2?} (changes: {})",
|
||||||
|
started.elapsed(),
|
||||||
|
changed
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(changed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ pub struct DirectoryInfo {
|
|||||||
impl DirectoryInfo {
|
impl DirectoryInfo {
|
||||||
// Get the settings directory
|
// Get the settings directory
|
||||||
// init() is not needed for this function
|
// init() is not needed for this function
|
||||||
|
// [AR] Patch fix. From PR.
|
||||||
pub fn get_initial_settings_dir() -> Option<PathBuf> {
|
pub fn get_initial_settings_dir() -> Option<PathBuf> {
|
||||||
Self::env_path("THESEUS_CONFIG_DIR").or_else(|| {
|
Self::env_path("THESEUS_CONFIG_DIR").or_else(|| {
|
||||||
if std::env::current_dir().ok()?.join("portable.txt").exists() {
|
if std::env::current_dir().ok()?.join("portable.txt").exists() {
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
|
// [AR] Feature
|
||||||
use std::{
|
use std::{
|
||||||
sync::{atomic::AtomicBool, Arc},
|
sync::{atomic::AtomicBool, Arc},
|
||||||
time::{SystemTime, UNIX_EPOCH}, // AstralRinth
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use discord_rich_presence::{
|
use discord_rich_presence::{
|
||||||
activity::{Activity, Assets, Timestamps}, // AstralRinth
|
activity::{Activity, Assets, Timestamps}, // [AR] Feature
|
||||||
DiscordIpc, DiscordIpcClient,
|
DiscordIpc, DiscordIpcClient,
|
||||||
};
|
};
|
||||||
use rand::seq::SliceRandom; // AstralRinth
|
use rand::seq::SliceRandom; // [AR] Feature
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
use crate::util::utils; // AstralRinth
|
use crate::util::utils; // [AR] Feature
|
||||||
use crate::State;
|
use crate::State;
|
||||||
|
|
||||||
pub struct DiscordGuard {
|
pub struct DiscordGuard {
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ where
|
|||||||
expires: legacy_credentials.expires,
|
expires: legacy_credentials.expires,
|
||||||
active: minecraft_auth.default_user == Some(uuid)
|
active: minecraft_auth.default_user == Some(uuid)
|
||||||
|| minecraft_users_len == 1,
|
|| minecraft_users_len == 1,
|
||||||
|
account_type: legacy_credentials.account_type,
|
||||||
}
|
}
|
||||||
.upsert(exec)
|
.upsert(exec)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -518,6 +519,7 @@ struct LegacyCredentials {
|
|||||||
pub access_token: String,
|
pub access_token: String,
|
||||||
pub refresh_token: String,
|
pub refresh_token: String,
|
||||||
pub expires: DateTime<Utc>,
|
pub expires: DateTime<Utc>,
|
||||||
|
pub account_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ pub async fn login_finish(
|
|||||||
expires: oauth_token.date
|
expires: oauth_token.date
|
||||||
+ Duration::seconds(oauth_token.value.expires_in as i64),
|
+ Duration::seconds(oauth_token.value.expires_in as i64),
|
||||||
active: true,
|
active: true,
|
||||||
|
account_type: AccountType::Microsoft.as_lowercase_str(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// During login, we need to fetch the online profile at least once to get the
|
// During login, we need to fetch the online profile at least once to get the
|
||||||
@@ -213,7 +214,7 @@ pub async fn login_finish(
|
|||||||
Ok(credentials)
|
Ok(credentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patched by AstralRinth
|
// [AR] Feature
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
pub async fn offline_auth(
|
pub async fn offline_auth(
|
||||||
name: &str,
|
name: &str,
|
||||||
@@ -229,6 +230,7 @@ pub async fn offline_auth(
|
|||||||
refresh_token: refresh_token,
|
refresh_token: refresh_token,
|
||||||
expires: Utc::now() + Duration::days(365 * 99),
|
expires: Utc::now() + Duration::days(365 * 99),
|
||||||
active: true,
|
active: true,
|
||||||
|
account_type: AccountType::Pirate.as_lowercase_str(),
|
||||||
};
|
};
|
||||||
|
|
||||||
credentials.offline_profile = MinecraftProfile {
|
credentials.offline_profile = MinecraftProfile {
|
||||||
@@ -242,6 +244,58 @@ pub async fn offline_auth(
|
|||||||
Ok(credentials)
|
Ok(credentials)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [AR] Feature
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn elyby_auth(
|
||||||
|
uuid: Uuid,
|
||||||
|
username: &str,
|
||||||
|
access_token: &str,
|
||||||
|
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite> + Copy,
|
||||||
|
) -> crate::Result<Credentials> {
|
||||||
|
let mut credentials = Credentials {
|
||||||
|
offline_profile: MinecraftProfile::default(),
|
||||||
|
access_token: access_token.to_string(),
|
||||||
|
refresh_token: "null".to_string(),
|
||||||
|
expires: Utc::now() + Duration::days(365 * 99),
|
||||||
|
active: true,
|
||||||
|
account_type: AccountType::ElyBy.as_lowercase_str(),
|
||||||
|
};
|
||||||
|
|
||||||
|
credentials.offline_profile = MinecraftProfile {
|
||||||
|
id: uuid,
|
||||||
|
name: username.to_string(),
|
||||||
|
..credentials.offline_profile
|
||||||
|
};
|
||||||
|
|
||||||
|
credentials.upsert(exec).await?;
|
||||||
|
|
||||||
|
Ok(credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [AR] • Feature
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub enum AccountType {
|
||||||
|
Unknown,
|
||||||
|
Microsoft,
|
||||||
|
Pirate,
|
||||||
|
ElyBy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountType {
|
||||||
|
fn as_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
AccountType::Unknown => "Unknown",
|
||||||
|
AccountType::Microsoft => "Microsoft",
|
||||||
|
AccountType::Pirate => "Pirate",
|
||||||
|
AccountType::ElyBy => "ElyBy",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn as_lowercase_str(&self) -> String {
|
||||||
|
self.as_str().to_lowercase()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
/// The offline profile of the user these credentials are for.
|
/// The offline profile of the user these credentials are for.
|
||||||
@@ -255,6 +309,7 @@ pub struct Credentials {
|
|||||||
pub refresh_token: String,
|
pub refresh_token: String,
|
||||||
pub expires: DateTime<Utc>,
|
pub expires: DateTime<Utc>,
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
|
pub account_type: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An entry in the player profile cache, keyed by player UUID.
|
/// An entry in the player profile cache, keyed by player UUID.
|
||||||
@@ -480,7 +535,7 @@ impl Credentials {
|
|||||||
let res = sqlx::query!(
|
let res = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
uuid, active, username, access_token, refresh_token, expires
|
uuid, active, username, access_token, refresh_token, expires, account_type
|
||||||
FROM minecraft_users
|
FROM minecraft_users
|
||||||
WHERE active = TRUE
|
WHERE active = TRUE
|
||||||
"
|
"
|
||||||
@@ -503,6 +558,7 @@ impl Credentials {
|
|||||||
.single()
|
.single()
|
||||||
.unwrap_or_else(Utc::now),
|
.unwrap_or_else(Utc::now),
|
||||||
active: x.active == 1,
|
active: x.active == 1,
|
||||||
|
account_type: x.account_type,
|
||||||
};
|
};
|
||||||
credentials.refresh(exec).await.ok();
|
credentials.refresh(exec).await.ok();
|
||||||
Some(credentials)
|
Some(credentials)
|
||||||
@@ -517,7 +573,7 @@ impl Credentials {
|
|||||||
let res = sqlx::query!(
|
let res = sqlx::query!(
|
||||||
"
|
"
|
||||||
SELECT
|
SELECT
|
||||||
uuid, active, username, access_token, refresh_token, expires
|
uuid, active, username, access_token, refresh_token, expires, account_type
|
||||||
FROM minecraft_users
|
FROM minecraft_users
|
||||||
"
|
"
|
||||||
)
|
)
|
||||||
@@ -537,6 +593,7 @@ impl Credentials {
|
|||||||
.single()
|
.single()
|
||||||
.unwrap_or_else(Utc::now),
|
.unwrap_or_else(Utc::now),
|
||||||
active: x.active == 1,
|
active: x.active == 1,
|
||||||
|
account_type: x.account_type,
|
||||||
};
|
};
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
@@ -572,14 +629,15 @@ impl Credentials {
|
|||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"
|
"
|
||||||
INSERT INTO minecraft_users (uuid, active, username, access_token, refresh_token, expires)
|
INSERT INTO minecraft_users (uuid, active, username, access_token, refresh_token, expires, account_type)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
ON CONFLICT (uuid) DO UPDATE SET
|
ON CONFLICT (uuid) DO UPDATE SET
|
||||||
active = $2,
|
active = $2,
|
||||||
username = $3,
|
username = $3,
|
||||||
access_token = $4,
|
access_token = $4,
|
||||||
refresh_token = $5,
|
refresh_token = $5,
|
||||||
expires = $6
|
expires = $6,
|
||||||
|
account_type = $7
|
||||||
",
|
",
|
||||||
uuid,
|
uuid,
|
||||||
self.active,
|
self.active,
|
||||||
@@ -587,6 +645,7 @@ impl Credentials {
|
|||||||
self.access_token,
|
self.access_token,
|
||||||
self.refresh_token,
|
self.refresh_token,
|
||||||
expires,
|
expires,
|
||||||
|
self.account_type,
|
||||||
)
|
)
|
||||||
.execute(exec)
|
.execute(exec)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -649,6 +708,7 @@ impl Serialize for Credentials {
|
|||||||
ser.serialize_field("refresh_token", &self.refresh_token)?;
|
ser.serialize_field("refresh_token", &self.refresh_token)?;
|
||||||
ser.serialize_field("expires", &self.expires)?;
|
ser.serialize_field("expires", &self.expires)?;
|
||||||
ser.serialize_field("active", &self.active)?;
|
ser.serialize_field("active", &self.active)?;
|
||||||
|
ser.serialize_field("account_type", &self.account_type)?;
|
||||||
ser.end()
|
ser.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -790,7 +850,7 @@ const MICROSOFT_CLIENT_ID: &str = "00000000402b5328";
|
|||||||
const REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
const REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf";
|
||||||
const REQUESTED_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
const REQUESTED_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
|
||||||
|
|
||||||
/* AstralRinth
|
/* [AR] Fix
|
||||||
* Weird visibility issue that didn't reproduce before
|
* Weird visibility issue that didn't reproduce before
|
||||||
* Had to make DeviceToken and RequestWithDate pub(crate) to fix compilation error
|
* Had to make DeviceToken and RequestWithDate pub(crate) to fix compilation error
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ pub mod fetch;
|
|||||||
pub mod io;
|
pub mod io;
|
||||||
pub mod jre;
|
pub mod jre;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
pub mod utils; // AstralRinth
|
pub mod utils; // [AR] Feature
|
||||||
pub mod server_ping;
|
pub mod server_ping;
|
||||||
|
|||||||
@@ -1,21 +1,535 @@
|
|||||||
|
use crate::api::update;
|
||||||
|
use crate::state::db;
|
||||||
|
///
|
||||||
|
/// [AR] Feature Utils
|
||||||
|
///
|
||||||
|
/// Version: 0.1.1
|
||||||
|
///
|
||||||
|
use crate::{Result, State};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::io;
|
use std::path::PathBuf;
|
||||||
|
use std::process;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
use tokio::{fs, io};
|
||||||
|
|
||||||
/*
|
|
||||||
AstralRinth Utils
|
|
||||||
*/
|
|
||||||
const PACKAGE_JSON_CONTENT: &str =
|
const PACKAGE_JSON_CONTENT: &str =
|
||||||
// include_str!("../../../../apps/app-frontend/package.json");
|
// include_str!("../../../../apps/app-frontend/package.json");
|
||||||
include_str!("../../../../apps/app/tauri.conf.json");
|
include_str!("../../../../apps/app/tauri.conf.json");
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct Launcher {
|
pub struct Launcher {
|
||||||
pub version: String
|
pub version: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_package_json() -> io::Result<Launcher> {
|
#[derive(Debug, Deserialize)]
|
||||||
// Deserialize the content of package.json into a Launcher struct
|
struct Artifact {
|
||||||
let launcher: Launcher = serde_json::from_str(PACKAGE_JSON_CONTENT)?;
|
path: Option<String>,
|
||||||
|
sha1: Option<String>,
|
||||||
|
url: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Downloads {
|
||||||
|
artifact: Option<Artifact>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Library {
|
||||||
|
name: String,
|
||||||
|
downloads: Option<Downloads>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct VersionJson {
|
||||||
|
libraries: Vec<Library>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserialize the content of package.json into a Launcher struct
|
||||||
|
pub fn read_package_json() -> io::Result<Launcher> {
|
||||||
|
let launcher: Launcher = serde_json::from_str(PACKAGE_JSON_CONTENT)?;
|
||||||
Ok(launcher)
|
Ok(launcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// ### AR • Ely.by Injector
|
||||||
|
/// Returns the PathBuf to the Ely.by AuthLib Injector
|
||||||
|
/// If resource doesn't exist or outdated, it will be downloaded from Git Astralium.
|
||||||
|
pub async fn get_or_download_elyby_injector() -> Result<PathBuf> {
|
||||||
|
tracing::info!("[AR] • Initializing state for Ely.by AuthLib Injector...");
|
||||||
|
let state = State::get().await?;
|
||||||
|
let libraries_dir = state.directories.libraries_dir();
|
||||||
|
|
||||||
|
// Stores the local authlib injectors from `libraries/astralrinth/authlib_injectors/` directory.
|
||||||
|
let mut local_authlib_injectors = Vec::new();
|
||||||
|
|
||||||
|
validate_astralrinth_library_dir(&libraries_dir, "authlib_injector/").await?;
|
||||||
|
let astralrinth_dir = libraries_dir.join("astralrinth/");
|
||||||
|
let authlib_injector_dir = astralrinth_dir.join("authlib_injector/");
|
||||||
|
let mut authlib_injector_dir_data = fs::read_dir(&authlib_injector_dir).await?;
|
||||||
|
|
||||||
|
// Get all local authlib injectors
|
||||||
|
while let Some(entry) = authlib_injector_dir_data.next_entry().await? {
|
||||||
|
let path = entry.path();
|
||||||
|
if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
|
||||||
|
if file_name.starts_with("authlib-injector") {
|
||||||
|
let metadata = entry.metadata().await?;
|
||||||
|
let modified = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
|
||||||
|
local_authlib_injectors.push((path.clone(), modified));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get information about latest authlib injector from remote repository
|
||||||
|
let (asset_name, download_url) = match extract_elyby_authlib_metadata("authlib-injector").await {
|
||||||
|
Ok(data) => data,
|
||||||
|
Err(err) => {
|
||||||
|
if let Some((local_path, _)) = local_authlib_injectors
|
||||||
|
.iter()
|
||||||
|
.max_by(|a, b| a.1.cmp(&b.1))
|
||||||
|
{
|
||||||
|
tracing::info!("[AR] • Found local AuthLib Injector(s):");
|
||||||
|
for (path, time) in &local_authlib_injectors {
|
||||||
|
tracing::info!("• {:?} (modified: {:?})", path.file_name().unwrap(), time);
|
||||||
|
}
|
||||||
|
tracing::warn!("[AR] • Failed to get latest AuthLib Injector from remote, using latest local version: {}", local_path.display());
|
||||||
|
return Ok(local_path.clone());
|
||||||
|
} else {
|
||||||
|
tracing::error!("[AR] • Failed to get AuthLib Injector from remote and no local copy found.");
|
||||||
|
return Err(crate::ErrorKind::NetworkErrorOccurred { error: format!("Failed to fetch authlib-injector metadata and no local version available: {}", err) }.as_error());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !local_authlib_injectors.is_empty() {
|
||||||
|
local_authlib_injectors.sort_by(|a, b| a.1.cmp(&b.1));
|
||||||
|
tracing::info!("[AR] • Found local AuthLib Injector(s):");
|
||||||
|
for (path, time) in &local_authlib_injectors {
|
||||||
|
tracing::info!("• {:?} (modified: {:?})", path.file_name().unwrap(), time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let remote_authlib_injector = if !asset_name.is_empty() {
|
||||||
|
authlib_injector_dir.join(&asset_name)
|
||||||
|
} else {
|
||||||
|
return Err(crate::ErrorKind::ParseError { reason: "Asset name is empty from metadata".to_string() }.as_error());
|
||||||
|
};
|
||||||
|
|
||||||
|
let latest_local_authlib_injector = local_authlib_injectors
|
||||||
|
.first()
|
||||||
|
.map(|(p, _)| p.clone());
|
||||||
|
|
||||||
|
let latest_local_authlib_injector_full_path_buf = match latest_local_authlib_injector {
|
||||||
|
Some(path) => path,
|
||||||
|
None => {
|
||||||
|
tracing::info!("[AR] • No local version found, will download from remote: {}", remote_authlib_injector.display());
|
||||||
|
let bytes = fetch_bytes_from_url(download_url.as_str()).await?;
|
||||||
|
write_file_to_libraries(&remote_authlib_injector.to_string_lossy(), &bytes).await?;
|
||||||
|
tracing::info!("[AR] • Successfully saved AuthLib Injector to {}", remote_authlib_injector.display());
|
||||||
|
return Ok(remote_authlib_injector);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::info!("[AR] • Remote Asset name: {}", asset_name);
|
||||||
|
tracing::info!("[AR] • Remote Download URL: {}", download_url);
|
||||||
|
tracing::info!("[AR] • Latest local AuthLib Injector: {}", latest_local_authlib_injector_full_path_buf.file_name().unwrap().to_string_lossy());
|
||||||
|
tracing::info!("[AR] • Comparing local version {} with parsed remote version {}", latest_local_authlib_injector_full_path_buf.display(), remote_authlib_injector.display());
|
||||||
|
|
||||||
|
if remote_authlib_injector == latest_local_authlib_injector_full_path_buf {
|
||||||
|
tracing::info!("[AR] • Remote version is the same as local version, using local copy.");
|
||||||
|
return Ok(latest_local_authlib_injector_full_path_buf);
|
||||||
|
} else {
|
||||||
|
tracing::info!(
|
||||||
|
"[AR] • Doesn't exist or outdated, attempting to download latest AuthLib Injector from URL: {}",
|
||||||
|
download_url
|
||||||
|
);
|
||||||
|
let bytes = fetch_bytes_from_url(download_url.as_str()).await?;
|
||||||
|
write_file_to_libraries(&remote_authlib_injector.to_string_lossy(), &bytes).await?;
|
||||||
|
tracing::info!("[AR] • Successfully saved AuthLib Injector to {}", remote_authlib_injector.display());
|
||||||
|
return Ok(remote_authlib_injector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### AR • Migration. Patch
|
||||||
|
/// Applying migration fix for SQLite database.
|
||||||
|
pub async fn apply_migration_fix(eol: &str) -> Result<bool> {
|
||||||
|
tracing::info!("[AR] • Attempting to apply migration fix");
|
||||||
|
let patched = db::apply_migration_fix(eol).await?;
|
||||||
|
if patched {
|
||||||
|
tracing::info!("[AR] • Successfully applied migration fix");
|
||||||
|
} else {
|
||||||
|
tracing::error!("[AR] • Failed to apply migration fix");
|
||||||
|
}
|
||||||
|
Ok(patched)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### AR • Feature. Updater
|
||||||
|
/// Initialize the update launcher.
|
||||||
|
pub async fn init_update_launcher(
|
||||||
|
download_url: &str,
|
||||||
|
local_filename: &str,
|
||||||
|
os_type: &str,
|
||||||
|
auto_update_supported: bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
tracing::info!("[AR] • Initialize downloading from • {:?}", download_url);
|
||||||
|
tracing::info!("[AR] • Save local file name • {:?}", local_filename);
|
||||||
|
tracing::info!("[AR] • OS type • {}", os_type);
|
||||||
|
tracing::info!("[AR] • Auto update supported • {}", auto_update_supported);
|
||||||
|
|
||||||
|
if let Err(e) = update::get_resource(
|
||||||
|
download_url,
|
||||||
|
local_filename,
|
||||||
|
os_type,
|
||||||
|
auto_update_supported,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"[AR] • An error occurred! Failed to download the file: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
println!("[AR] • Code finishes without errors.");
|
||||||
|
process::exit(0)
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### AR • AuthLib (Ely.by)
|
||||||
|
/// Initializes the AuthLib patching process.
|
||||||
|
///
|
||||||
|
/// Returns `true` if the authlib patched successfully.
|
||||||
|
pub async fn init_authlib_patching(
|
||||||
|
minecraft_version: &str,
|
||||||
|
is_mojang: bool,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let minecraft_library_metadata =
|
||||||
|
get_minecraft_library_metadata(minecraft_version).await?;
|
||||||
|
// Parses the AuthLib version from string
|
||||||
|
// Example output: "com.mojang:authlib:6.0.58" -> "6.0.58"
|
||||||
|
let authlib_version = minecraft_library_metadata
|
||||||
|
.name
|
||||||
|
.split(':')
|
||||||
|
.nth(2)
|
||||||
|
.unwrap_or("unknown");
|
||||||
|
let authlib_fullname_string = format!("authlib-{}.jar", authlib_version);
|
||||||
|
let authlib_fullname_str = authlib_fullname_string.as_str();
|
||||||
|
|
||||||
|
tracing::info!(
|
||||||
|
"[AR] • Attempting to download AuthLib -> {}.",
|
||||||
|
authlib_fullname_string
|
||||||
|
);
|
||||||
|
|
||||||
|
download_authlib(
|
||||||
|
&minecraft_library_metadata,
|
||||||
|
authlib_fullname_str,
|
||||||
|
minecraft_version,
|
||||||
|
is_mojang,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### AR • Universal Write (IO) Function.
|
||||||
|
/// Validating the `astralrinth/{target_directory}/` directory exists inside the libraries/astralrinth directory.
|
||||||
|
async fn validate_astralrinth_library_dir(
|
||||||
|
libraries_dir: &PathBuf,
|
||||||
|
validation_directory: &str
|
||||||
|
) -> Result<()> {
|
||||||
|
let astralrinth_path = libraries_dir.join(format!("astralrinth/{}", validation_directory));
|
||||||
|
if !astralrinth_path.exists() {
|
||||||
|
tokio::fs::create_dir_all(&astralrinth_path)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!(
|
||||||
|
"[AR] • Failed to create {} directory: {:?}",
|
||||||
|
astralrinth_path.display(),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
crate::ErrorKind::IOErrorOccurred {
|
||||||
|
error: format!(
|
||||||
|
"Failed to create {} directory: {}",
|
||||||
|
astralrinth_path.display(),
|
||||||
|
e
|
||||||
|
),
|
||||||
|
}
|
||||||
|
.as_error()
|
||||||
|
})?;
|
||||||
|
tracing::info!(
|
||||||
|
"[AR] • Created missing {} directory",
|
||||||
|
astralrinth_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### AR • Universal Write (IO) Function
|
||||||
|
/// Saves the downloaded bytes to the `libraries` directory using the given relative path.
|
||||||
|
async fn write_file_to_libraries(
|
||||||
|
relative_path: &str,
|
||||||
|
bytes: &bytes::Bytes,
|
||||||
|
) -> Result<()> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let output_path = state.directories.libraries_dir().join(relative_path);
|
||||||
|
|
||||||
|
fs::write(&output_path, bytes).await.map_err(|e| {
|
||||||
|
tracing::error!("[AR] • Failed to save file: {:?}", e);
|
||||||
|
crate::ErrorKind::IOErrorOccurred {
|
||||||
|
error: format!("Failed to save file: {e}"),
|
||||||
|
}
|
||||||
|
.as_error()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### AR • AuthLib (Ely.by)
|
||||||
|
/// Downloads the AuthLib file from Mojang libraries or Git Astralium services.
|
||||||
|
async fn download_authlib(
|
||||||
|
minecraft_library_metadata: &Library,
|
||||||
|
authlib_fullname: &str,
|
||||||
|
minecraft_version: &str,
|
||||||
|
is_mojang: bool,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let (mut url, path) = extract_minecraft_local_download_info(
|
||||||
|
minecraft_library_metadata,
|
||||||
|
minecraft_version,
|
||||||
|
)?;
|
||||||
|
let full_path = state.directories.libraries_dir().join(path);
|
||||||
|
|
||||||
|
if !is_mojang {
|
||||||
|
tracing::info!(
|
||||||
|
"[AR] • Attempting to download AuthLib from Git Astralium"
|
||||||
|
);
|
||||||
|
(_, url) = extract_elyby_authlib_metadata(authlib_fullname).await?;
|
||||||
|
}
|
||||||
|
tracing::info!("[AR] • Downloading AuthLib from URL: {}", url);
|
||||||
|
let bytes = fetch_bytes_from_url(&url).await?;
|
||||||
|
tracing::info!("[AR] • Will save to path: {}", full_path.to_str().unwrap());
|
||||||
|
write_file_to_libraries(full_path.to_str().unwrap(), &bytes).await?;
|
||||||
|
tracing::info!("[AR] • Successfully saved AuthLib to {:?}", full_path);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### AR • AuthLib (Ely.by)
|
||||||
|
/// Parses the ElyIntegration release JSON and returns the download URL for the given AuthLib version.
|
||||||
|
async fn extract_elyby_authlib_metadata(
|
||||||
|
authlib_fullname: &str,
|
||||||
|
) -> Result<(String, String)> {
|
||||||
|
const URL: &str = "https://git.astralium.su/api/v1/repos/didirus/ElyIntegration/releases/latest";
|
||||||
|
|
||||||
|
let response = reqwest::get(URL).await.map_err(|e| {
|
||||||
|
tracing::error!(
|
||||||
|
"[AR] • Failed to fetch ElyIntegration release JSON: {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
crate::ErrorKind::NetworkErrorOccurred {
|
||||||
|
error: format!(
|
||||||
|
"Failed to fetch ElyIntegration release JSON: {}",
|
||||||
|
e
|
||||||
|
),
|
||||||
|
}
|
||||||
|
.as_error()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let json: serde_json::Value = response.json().await.map_err(|e| {
|
||||||
|
tracing::error!("[AR] • Failed to parse ElyIntegration JSON: {:?}", e);
|
||||||
|
crate::ErrorKind::ParseError {
|
||||||
|
reason: format!("Failed to parse ElyIntegration JSON: {}", e),
|
||||||
|
}
|
||||||
|
.as_error()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let assets =
|
||||||
|
json.get("assets")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
crate::ErrorKind::ParseError {
|
||||||
|
reason: "Missing 'assets' array".into(),
|
||||||
|
}
|
||||||
|
.as_error()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let asset = assets
|
||||||
|
.iter()
|
||||||
|
.find(|a| {
|
||||||
|
a.get("name")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.map(|n| n.contains(authlib_fullname))
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| {
|
||||||
|
crate::ErrorKind::ParseError {
|
||||||
|
reason: format!(
|
||||||
|
"No matching asset for {} in ElyIntegration JSON response.",
|
||||||
|
authlib_fullname
|
||||||
|
),
|
||||||
|
}
|
||||||
|
.as_error()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let download_url = asset
|
||||||
|
.get("browser_download_url")
|
||||||
|
.and_then(|u| u.as_str())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
crate::ErrorKind::ParseError {
|
||||||
|
reason: "Missing 'browser_download_url'".into(),
|
||||||
|
}
|
||||||
|
.as_error()
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let asset_name = asset
|
||||||
|
.get("name")
|
||||||
|
.and_then(|n| n.as_str())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
crate::ErrorKind::ParseError {
|
||||||
|
reason: "Missing 'name'".into(),
|
||||||
|
}
|
||||||
|
.as_error()
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
Ok((asset_name, download_url))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### AR • AuthLib (Ely.by)
|
||||||
|
/// Extracts the artifact URL and Path from the library structure.
|
||||||
|
///
|
||||||
|
/// Returns a tuple of references to the URL and path strings,
|
||||||
|
/// or an error if the required metadata is missing.
|
||||||
|
fn extract_minecraft_local_download_info(
|
||||||
|
minecraft_library_metadata: &Library,
|
||||||
|
minecraft_version: &str,
|
||||||
|
) -> Result<(String, String)> {
|
||||||
|
let artifact = minecraft_library_metadata
|
||||||
|
.downloads
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|d| d.artifact.as_ref())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
crate::ErrorKind::MinecraftMetadataNotFound {
|
||||||
|
minecraft_version: minecraft_version.to_string(),
|
||||||
|
}
|
||||||
|
.as_error()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let url = artifact.url.clone().ok_or_else(|| {
|
||||||
|
crate::ErrorKind::MinecraftMetadataNotFound {
|
||||||
|
minecraft_version: minecraft_version.to_string(),
|
||||||
|
}
|
||||||
|
.as_error()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let path = artifact.path.clone().ok_or_else(|| {
|
||||||
|
crate::ErrorKind::MinecraftMetadataNotFound {
|
||||||
|
minecraft_version: minecraft_version.to_string(),
|
||||||
|
}
|
||||||
|
.as_error()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((url, path))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### AR • Universal Fetch Bytes (IO)
|
||||||
|
/// Downloads bytes from the provided URL with a 15 second timeout.
|
||||||
|
async fn fetch_bytes_from_url(url: &str) -> Result<bytes::Bytes> {
|
||||||
|
// Create client instance with request timeout.
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
const TIMEOUT_SECONDS: u64 = 15;
|
||||||
|
|
||||||
|
let response = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(TIMEOUT_SECONDS),
|
||||||
|
client.get(url).send(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
tracing::error!(
|
||||||
|
"[AR] • Download timed out after {} seconds",
|
||||||
|
TIMEOUT_SECONDS
|
||||||
|
);
|
||||||
|
crate::ErrorKind::NetworkErrorOccurred {
|
||||||
|
error: format!(
|
||||||
|
"Download timed out after {TIMEOUT_SECONDS} seconds"
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
}
|
||||||
|
.as_error()
|
||||||
|
})?
|
||||||
|
.map_err(|e| {
|
||||||
|
tracing::error!("[AR] • Request error: {:?}", e);
|
||||||
|
crate::ErrorKind::NetworkErrorOccurred {
|
||||||
|
error: format!("Request error: {e}"),
|
||||||
|
}
|
||||||
|
.as_error()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !response.status().is_success() {
|
||||||
|
let status = response.status().to_string();
|
||||||
|
tracing::error!("[AR] • Failed to download file: HTTP {}", status);
|
||||||
|
return Err(crate::ErrorKind::NetworkErrorOccurred {
|
||||||
|
error: format!("Failed to download file: HTTP {status}"),
|
||||||
|
}
|
||||||
|
.as_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
response.bytes().await.map_err(|e| {
|
||||||
|
tracing::error!("[AR] • Failed to read response bytes: {:?}", e);
|
||||||
|
crate::ErrorKind::NetworkErrorOccurred {
|
||||||
|
error: format!("Failed to read response bytes: {e}"),
|
||||||
|
}
|
||||||
|
.as_error()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ### AR • AuthLib (Ely.by)
|
||||||
|
/// Gets the Minecraft library metadata from the local libraries directory.
|
||||||
|
async fn get_minecraft_library_metadata(
|
||||||
|
minecraft_version: &str,
|
||||||
|
) -> Result<Library> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
|
||||||
|
let path = state
|
||||||
|
.directories
|
||||||
|
.version_dir(minecraft_version)
|
||||||
|
.join(format!("{}.json", minecraft_version));
|
||||||
|
if !path.exists() {
|
||||||
|
tracing::error!("[AR] • File not found: {:#?}", path);
|
||||||
|
return Err(crate::ErrorKind::InvalidMinecraftVersion {
|
||||||
|
minecraft_version: minecraft_version.to_string(),
|
||||||
|
}
|
||||||
|
.as_error());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = fs::read_to_string(&path).await?;
|
||||||
|
let version_data: VersionJson = serde_json::from_str(&content)?;
|
||||||
|
|
||||||
|
for lib in version_data.libraries {
|
||||||
|
if lib.name.contains("com.mojang:authlib") {
|
||||||
|
if let Some(downloads) = &lib.downloads {
|
||||||
|
if let Some(artifact) = &downloads.artifact {
|
||||||
|
if artifact.path.is_some()
|
||||||
|
&& artifact.url.is_some()
|
||||||
|
&& artifact.sha1.is_some()
|
||||||
|
{
|
||||||
|
tracing::info!("[AR] • Found AuthLib: {}", lib.name);
|
||||||
|
tracing::info!(
|
||||||
|
"[AR] • Path: {}",
|
||||||
|
artifact.path.as_ref().unwrap()
|
||||||
|
);
|
||||||
|
tracing::info!(
|
||||||
|
"[AR] • URL: {}",
|
||||||
|
artifact.url.as_ref().unwrap()
|
||||||
|
);
|
||||||
|
tracing::info!(
|
||||||
|
"[AR] • SHA1: {}",
|
||||||
|
artifact.sha1.as_ref().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(lib);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(crate::ErrorKind::MinecraftMetadataNotFound {
|
||||||
|
minecraft_version: minecraft_version.to_string(),
|
||||||
|
}
|
||||||
|
.as_error())
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import _BellRingIcon from './icons/bell-ring.svg?component'
|
|||||||
import _BellIcon from './icons/bell.svg?component'
|
import _BellIcon from './icons/bell.svg?component'
|
||||||
import _BlocksIcon from './icons/blocks.svg?component'
|
import _BlocksIcon from './icons/blocks.svg?component'
|
||||||
import _BoldIcon from './icons/bold.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 _BookTextIcon from './icons/book-text.svg?component'
|
||||||
import _BookIcon from './icons/book.svg?component'
|
import _BookIcon from './icons/book.svg?component'
|
||||||
import _BookmarkIcon from './icons/bookmark.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 _BoxImportIcon from './icons/box-import.svg?component'
|
||||||
import _BoxIcon from './icons/box.svg?component'
|
import _BoxIcon from './icons/box.svg?component'
|
||||||
import _BracesIcon from './icons/braces.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 _CalendarIcon from './icons/calendar.svg?component'
|
||||||
import _CardIcon from './icons/card.svg?component'
|
import _CardIcon from './icons/card.svg?component'
|
||||||
import _ChangeSkinIcon from './icons/change-skin.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 _IssuesIcon from './icons/issues.svg?component'
|
||||||
import _ItalicIcon from './icons/italic.svg?component'
|
import _ItalicIcon from './icons/italic.svg?component'
|
||||||
import _KeyIcon from './icons/key.svg?component'
|
import _KeyIcon from './icons/key.svg?component'
|
||||||
|
import _KeyboardIcon from './icons/keyboard.svg?component'
|
||||||
import _LanguagesIcon from './icons/languages.svg?component'
|
import _LanguagesIcon from './icons/languages.svg?component'
|
||||||
import _LeftArrowIcon from './icons/left-arrow.svg?component'
|
import _LeftArrowIcon from './icons/left-arrow.svg?component'
|
||||||
import _LibraryIcon from './icons/library.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 _TimerIcon from './icons/timer.svg?component'
|
||||||
import _TransferIcon from './icons/transfer.svg?component'
|
import _TransferIcon from './icons/transfer.svg?component'
|
||||||
import _TrashIcon from './icons/trash.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 _UnderlineIcon from './icons/underline.svg?component'
|
||||||
import _UndoIcon from './icons/undo.svg?component'
|
import _UndoIcon from './icons/undo.svg?component'
|
||||||
import _UnknownDonationIcon from './icons/unknown-donation.svg?component'
|
import _UnknownDonationIcon from './icons/unknown-donation.svg?component'
|
||||||
@@ -199,6 +203,7 @@ export const BellRingIcon = _BellRingIcon
|
|||||||
export const BellIcon = _BellIcon
|
export const BellIcon = _BellIcon
|
||||||
export const BlocksIcon = _BlocksIcon
|
export const BlocksIcon = _BlocksIcon
|
||||||
export const BoldIcon = _BoldIcon
|
export const BoldIcon = _BoldIcon
|
||||||
|
export const BookOpenIcon = _BookOpenIcon
|
||||||
export const BookTextIcon = _BookTextIcon
|
export const BookTextIcon = _BookTextIcon
|
||||||
export const BookIcon = _BookIcon
|
export const BookIcon = _BookIcon
|
||||||
export const BookmarkIcon = _BookmarkIcon
|
export const BookmarkIcon = _BookmarkIcon
|
||||||
@@ -206,6 +211,7 @@ export const BotIcon = _BotIcon
|
|||||||
export const BoxImportIcon = _BoxImportIcon
|
export const BoxImportIcon = _BoxImportIcon
|
||||||
export const BoxIcon = _BoxIcon
|
export const BoxIcon = _BoxIcon
|
||||||
export const BracesIcon = _BracesIcon
|
export const BracesIcon = _BracesIcon
|
||||||
|
export const BrushCleaningIcon = _BrushCleaningIcon
|
||||||
export const CalendarIcon = _CalendarIcon
|
export const CalendarIcon = _CalendarIcon
|
||||||
export const CardIcon = _CardIcon
|
export const CardIcon = _CardIcon
|
||||||
export const ChangeSkinIcon = _ChangeSkinIcon
|
export const ChangeSkinIcon = _ChangeSkinIcon
|
||||||
@@ -273,6 +279,7 @@ export const InfoIcon = _InfoIcon
|
|||||||
export const IssuesIcon = _IssuesIcon
|
export const IssuesIcon = _IssuesIcon
|
||||||
export const ItalicIcon = _ItalicIcon
|
export const ItalicIcon = _ItalicIcon
|
||||||
export const KeyIcon = _KeyIcon
|
export const KeyIcon = _KeyIcon
|
||||||
|
export const KeyboardIcon = _KeyboardIcon
|
||||||
export const LanguagesIcon = _LanguagesIcon
|
export const LanguagesIcon = _LanguagesIcon
|
||||||
export const LeftArrowIcon = _LeftArrowIcon
|
export const LeftArrowIcon = _LeftArrowIcon
|
||||||
export const LibraryIcon = _LibraryIcon
|
export const LibraryIcon = _LibraryIcon
|
||||||
@@ -353,6 +360,7 @@ export const TextQuoteIcon = _TextQuoteIcon
|
|||||||
export const TimerIcon = _TimerIcon
|
export const TimerIcon = _TimerIcon
|
||||||
export const TransferIcon = _TransferIcon
|
export const TransferIcon = _TransferIcon
|
||||||
export const TrashIcon = _TrashIcon
|
export const TrashIcon = _TrashIcon
|
||||||
|
export const TriangleAlertIcon = _TriangleAlertIcon
|
||||||
export const UnderlineIcon = _UnderlineIcon
|
export const UnderlineIcon = _UnderlineIcon
|
||||||
export const UndoIcon = _UndoIcon
|
export const UndoIcon = _UndoIcon
|
||||||
export const UnknownDonationIcon = _UnknownDonationIcon
|
export const UnknownDonationIcon = _UnknownDonationIcon
|
||||||
|
|||||||
1
packages/assets/icons/book-open.svg
Normal file
1
packages/assets/icons/book-open.svg
Normal file
@@ -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 |
9
packages/assets/icons/brush-cleaning.svg
Normal file
9
packages/assets/icons/brush-cleaning.svg
Normal file
@@ -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 |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user