47 Commits

Author SHA1 Message Date
2d5d747202 update readme markdown files, added RUS language 2025-07-16 13:49:50 +03:00
7516ff9e47 Merge pull request 'feature-elyby-skins' (#8) from feature-elyby-skins into beta
Reviewed-on: #8
2025-07-16 13:13:03 +03:00
df9bbe3ba0 chore: add patch file to patches directory 2025-07-16 02:31:59 +03:00
362fd7f32a feat: Implement Ely By skin system
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Successful in 44m33s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
2025-07-16 02:27:48 +03:00
adf831dab9 Merge commit 'efeac22d14fb6e782869d56d5d65685667721e4a' into feature-elyby-skins
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Failing after 3m24s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
2025-07-11 04:41:14 +03:00
efeac22d14 Merge pull request 'feature-improve-updater' (#6) from feature-improve-updater into beta
Reviewed-on: #6
2025-07-11 04:10:01 +03:00
591d98a9eb fix: crlf hash?
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Failing after 26m48s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
2025-07-11 03:56:11 +03:00
77472d9a09 fix: crlf hash?
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Failing after 1m18s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
2025-07-11 03:46:33 +03:00
789d666515 refactor: windows auto updater only works with signed app
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Failing after 1m18s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
2025-07-11 03:26:04 +03:00
d917bff6ef feat: add ability to auto exec downloaded installer on windows; minor changes
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Failing after 6m20s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
2025-07-11 03:04:37 +03:00
4e69cd8bde feat: add auto application restart after migration successful fix attempt
Some checks failed
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Has been cancelled
2025-07-11 02:38:23 +03:00
b71e4cc6f9 refactor: update checker moved to App.vue, added new animated icons
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Failing after 1m22s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
2025-07-11 02:29:05 +03:00
a56ab6adb9 refactor: move updates to settings
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Failing after 26m38s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
2025-07-11 01:34:31 +03:00
f1b67c9584 refactor: improve ErrorModal.vue 2025-07-10 23:12:47 +03:00
3d32640b83 refactor: comments 2025-07-10 21:32:44 +03:00
6dfb599e14 Merge pull request 'feature-another-migration-fix' (#5) from feature-another-migration-fix into beta
Reviewed-on: #5
2025-07-10 21:18:40 +03:00
332a543f66 fix: added ability for regenerate checksums with issued mr migrations.
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Failing after 34m13s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
2025-07-10 21:09:06 +03:00
1ef96c447e ci: patch validating git config on windows runner
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Failing after 3h10m8s
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
2025-07-10 16:30:04 +03:00
1ec92b5f97 ci: add steps with LF & CRLF checks
Some checks failed
AstralRinth App build / Build (x86_64-pc-windows-msvc, windows-latest) (push) Has been cancelled
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Has been cancelled
2025-07-10 15:59:17 +03:00
f0a4532051 ci: update astralrinth-build.yml 2025-07-10 15:53:26 +03:00
14f6450cf4 Merge commit '14bf06e4bd7a70a2c6483e6bbb9712cb9ff84486' into feature-elyby-skins
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Has been cancelled
2025-07-10 01:07:43 +03:00
14bf06e4bd Merge commit 'cb72d2ac80910cf01c9d2025d04d772fb8397abd' into beta 2025-07-10 01:07:09 +03:00
IMB11
cb72d2ac80 Skins improvements/fixes (#3943)
* feat: only initialize batch renderer if needed & head storage

* feat: support webp storage of skin renders if supported (falls back to png if not)

* fix: performance improvements with cache loading+saving

* fix: mirrored skins + remove cape model for embedded cape

* feat: antialiasing

* fix: leg jumping & store fbx's for reference

* fix: lint issues

* fix: lint issues

* feat: tweaks to radial spotlight

* fix: app nav btn colors
2025-07-09 21:41:36 +00:00
Nitrrine
3c79607d1f feat(app): increase logs card height (#3953) 2025-07-09 21:39:51 +00:00
97bd18c7b3 Merge commit '8af0288274bedbd78db03530b10aca0c5a3f13f1' into feature-elyby-skins 2025-07-10 00:01:12 +03:00
8af0288274 Merge commit '36ad1f16e46333cd85d4719d8ecfcfd745167075' into beta 2025-07-09 23:57:16 +03:00
167072de0c Merge pull request 'feat: added fake host for bypass minecraft account type (MSA) check on 1.16.4/5.' (#4) from feature-mp-button-fix into beta
Reviewed-on: #4
2025-07-09 23:49:07 +03:00
2df37be9a7 feat: added fake host for bypass minecraft account type (MSA) check on 1.16.4/5.
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Failing after 29m35s
2025-07-09 23:45:21 +03:00
34d85a03b2 Merge commit '17cf5e31321ef9c3a4f95489eb18d33818fb2090' into feature-elyby-skins
All checks were successful
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Successful in 35m22s
2025-07-09 22:58:27 +03:00
17cf5e3132 Merge pull request 'feature-fix-db' (#3) from feature-fix-db into beta
Reviewed-on: #3
2025-07-09 00:56:09 +03:00
Alejandro González
36ad1f16e4 ci(theseus): assorted tweaks and fixes (#3949)
* ci(theseus-build): ensure only relevant bundle artifacts are uploaded

Tauri leaves behind quite a bit of intermediate garbage in these target
folders, even when building with no build cache.

* ci(theseus-release): fix typo in RPM package URL generation

* ci(theseus-build): generate shorter and more user-friendly commit build versions
2025-07-08 21:02:17 +00:00
Prospector
5d4f334505 Update changelog 2025-07-08 13:57:06 -07:00
Prospector
1fdb5ba748 Add authors to blog posts and shorten some summaries (#3940) 2025-07-08 20:48:27 +00:00
c5e67a5c6f fix: typo
All checks were successful
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Successful in 41m31s
2025-07-08 23:43:50 +03:00
e2e21c1496 fix: another try to fix x86 windows arch
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Has been cancelled
2025-07-08 23:40:55 +03:00
6da942ccbb fix: Ignore x86 windows arch
Some checks failed
AstralRinth App build / Build (x86_64-unknown-linux-gnu, ubuntu-latest) (push) Has been cancelled
2025-07-08 23:32:33 +03:00
IMB11
26df6f51ef fix: composable used outside ... issue + disable cache (#3947) 2025-07-08 20:09:36 +00:00
Alejandro González
6caf794ae1 dist(docker): add curl package to Labrinth image, some other minor tweaks (#3915)
* dist(docker): add `.dockerignore` as symlink to `.gitignore`

This ensures that no files outside of version control are transferred to
the Docker build context for Labrinth and Daedalus images, which
significantly improves build speed (if a `target` directory is already
present) and build reproducibility.

* chore(dist/docker): simplify out unneeeded statements, move `SQLX_OFFLINE` env var setting to build command itself

The latter approach ensures that developers building the image locally
don't forget to set `SQLX_OFFLINE`, too.

* dist(docker): add `curl` package to Labrinth image
2025-07-08 19:22:15 +00:00
Alejandro González
2692953e31 fix(app): make Party Alex bonus default skin have slim arms (#3945)
This skin was incorrectly declared as having wide arms. Resolves #3941.
2025-07-08 19:03:30 +00:00
Prospector
242fd713ab Update changelog 2025-07-08 11:06:50 -07:00
IMB11
7a12c4d5e2 feat: reimplement error handling improvements w/o polling (#3942)
* Reapply "fix: error handling improvements (#3797)"

This reverts commit e0cde2d6ff.

* fix: polling issues

* fix: circuit breaker logic for spam

* fix: remove polling & ping test node instead

* fix: remove broken url from debugging

* fix: show error information display if node access fails in fs module
2025-07-08 17:40:44 +00:00
0ab4dec62d Merge pull request 'v0.10.302' (#2) from feature-clean into beta
Reviewed-on: #2
2025-07-08 18:00:08 +03:00
Prospector
f256ef43c0 Add x-archon-request header 2025-07-07 22:16:26 -07:00
Prospector
e0cde2d6ff Revert "fix: error handling improvements (#3797)"
This reverts commit 706976439d.
2025-07-07 17:37:43 -07:00
Prospector
e4e77dc0d2 Revert "temp: do not retry MRS requests"
This reverts commit 8ba6467f21.
2025-07-07 17:07:27 -07:00
Prospector
8ba6467f21 temp: do not retry MRS requests 2025-07-07 16:49:17 -07:00
Josiah Glosson
088cb54317 Fix failure when "Test"ing a Java installation (#3935)
* Fix failure when "Test"ing a Java installation

* Fix lint
2025-07-07 19:11:36 +00:00
115 changed files with 7318 additions and 1973 deletions

1
.dockerignore Symbolic link
View File

@@ -0,0 +1 @@
.gitignore

View File

@@ -16,6 +16,7 @@ on:
- 'packages/assets/**'
- 'packages/ui/**'
- 'packages/utils/**'
workflow_dispatch:
jobs:
build:
@@ -24,12 +25,12 @@ jobs:
fail-fast: false
matrix:
# platform: [macos-latest, windows-latest, ubuntu-latest]
platform: [ubuntu-latest]
platform: [windows-latest, ubuntu-latest]
include:
# - platform: macos-latest
# artifact-target-name: universal-apple-darwin
# - platform: windows-latest
# artifact-target-name: x86_64-pc-windows-msvc
- platform: windows-latest
artifact-target-name: x86_64-pc-windows-msvc
- platform: ubuntu-latest
artifact-target-name: x86_64-unknown-linux-gnu
@@ -41,6 +42,35 @@ jobs:
with:
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
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
@@ -73,11 +103,11 @@ jobs:
- name: 🧰 Install dependencies
run: pnpm install
# - name: ✍️ Set up Windows code signing (jsign)
# if: matrix.platform == 'windows-latest' && env.SIGN_WINDOWS_BINARIES == 'true'
# shell: bash
# run: |
# choco install jsign --ignore-dependencies
- name: ✍️ Set up Windows code signing (jsign)
if: matrix.platform == 'windows-latest' && env.SIGN_WINDOWS_BINARIES == 'true'
shell: bash
run: |
choco install jsign --ignore-dependencies
- name: 🗑️ Clean up cached bundles
shell: bash
@@ -99,15 +129,15 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
# - name: 🔨 Build Windows app
# if: matrix.platform == 'windows-latest'
# shell: pwsh
# run: |
# $env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
# pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis'
# env:
# TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
# TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: 🔨 Build Windows app
if: matrix.platform == 'windows-latest'
shell: pwsh
run: |
$env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis'
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: 📤 Upload app bundles
uses: actions/upload-artifact@v3

159
README.md
View File

@@ -1,76 +1,123 @@
# Navigation in this README
- [Install instructions](#install-instructions)
- [Features](#features)
- [Getting started](#getting-started)
- [Disclaimer](#disclaimer)
- [Donate](#support-our-project-crypto-wallets)
# 📘 Navigation
- [🔧 Install Instructions](#install-instructions)
- [✨ Features](#features)
- [🚀 Getting Started](#getting-started)
- [⚠️ 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
## 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.
## **AstralRinth • Empowering Your Minecraft Adventure**
## About Software
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.
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 Modrinths API, **Theseus Core** is your launchpad into a new era of Minecraft gameplay.
## AR • Unlocking Minecraft's Boundless Horizon
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:
- *Recently, improved integration with the Git Astralium API has been added.*
# Install instructions
- 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
## **About the Software**
### Downloadable file extensions
- `.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
**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.
### Installation subjects
- Builds in releases that are signed with the following prefixes are not recommended for installation and may contain errors:
- `dev`
- `nightly`
- `dirty`
- `dirty-dev`
- `dirty-nightly`
- `dirty_dev`
- `dirty_nightly`
- Auto-updating takes place through parsing special versions from releases, so we also distribute clean types of `.msi, .dmg and .deb`
## **AR • Unlocking Minecraft's Boundless Horizon**
This unique fork introduces a **free trial Minecraft experience**, bypassing license checks while maintaining rich functionality. Currently includes:
---
# Install Instructions
To install the launcher:
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
### Featured enhancement in AR
- 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_).
> _The launcher provides an opportunity to use the well-known Modrinth, but with an improved user experience._
### Easy to use
- Using the launcher is intuitive, any user can figure it out.
## Included exclusive features
### Update notifies
- 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.
- No ads in the entire launcher.
- 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
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.
- **Choosing the Correct File**: Ensure you select the file that matches your OS requirements.
- [**How select file**](#downloadable-file-extensions)
- [**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.
3. **Launch Minecraft**: Start your journey by launching Minecraft through AstralRinth and enjoy the adventures that await.
- **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.
To begin using AstralRinth:
1. **Download Your OS Version**
- Go to the [releases page](https://git.astralium.su/didirus/AstralRinth/releases)
- [How to choose a file](#downloadable-file-extensions)
- [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
- 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
- USDT TRC20 (Telegram): TMSmv1D5Fdf4fipUpwBCdh16WevrV45vGr
- TONCOIN (Telegram): UQAqUJ2_hVBI6k_gPyfp_jd-1K0OS61nIFPZuJWN9BwGAvKe
- TONCOIN (Telegram): UQAqUJ2_hVBI6k_gPyfp_jd-1K0OS61nIFPZuJWN9BwGAvKe

View File

@@ -42,7 +42,7 @@ import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
import { handleError, useNotifications } from '@/store/notifications.js'
import { command_listener, warning_listener } from '@/helpers/events.js'
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 { getCurrentWindow } from '@tauri-apps/api/window'
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 { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
// [AR] Feature
import { getRemote, updateState } from '@/helpers/update.js'
const themeStore = useTheming()
const news = ref([])
@@ -99,6 +102,7 @@ const isMaximized = ref(false)
onMounted(async () => {
await useCheckDisableMouseover()
await getRemote(false) // [AR] Check for updates
document.querySelector('body').addEventListener('click', handleClick)
document.querySelector('body').addEventListener('auxclick', handleAuxClick)
@@ -161,11 +165,11 @@ async function setupApp() {
initAnalytics()
if (!telemetry) {
console.info("[AR] Telemetry disabled by default (Hard patched).")
console.info("[AR] Telemetry disabled by default (Hard patched).")
optOutAnalytics()
}
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()
@@ -188,7 +192,7 @@ async function setupApp() {
}),
)
// Patched by AstralRinth
/// [AR] Patch
// useFetch(
// `https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`,
// 'criticalAnnouncements',
@@ -465,12 +469,20 @@ function handleAuxClick(e) {
<PlusIcon />
</NavButton>
<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 />
</NavButton>
<NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
<SettingsIcon />
</NavButton>
</NavButton> -->
<template v-if="updateState">
<NavButton class="neon-icon pulse" v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
<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>
<OverflowMenu
:options="[
@@ -501,13 +513,13 @@ function handleAuxClick(e) {
<!-- <ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" /> -->
<div class="flex items-center gap-1 ml-3">
<button
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
@click="router.back()"
>
<LeftArrowIcon />
</button>
<button
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
@click="router.forward()"
>
<RightArrowIcon />
@@ -659,6 +671,9 @@ function handleAuxClick(e) {
</template>
<style lang="scss" scoped>
@import '../../../packages/assets/styles/neon-icon.scss';
@import '../../../packages/assets/styles/neon-text.scss';
.window-controls {
z-index: 20;
display: none;

View File

@@ -50,9 +50,8 @@
color="primary"
@click="login()"
>
<LogInIcon v-if="!loginDisabled" />
<MicrosoftIcon v-if="!loginDisabled"/>
<SpinnerIcon v-else class="animate-spin" />
<MicrosoftIcon/>
</Button>
<Button v-tooltip="'Add offline'" icon-only @click="tryOfflineLogin()">
<PirateIcon />
@@ -153,11 +152,11 @@ const loginErrorModal = ref(null)
const unexpectedErrorModal = ref(null)
const playerName = ref('')
async function tryOfflineLogin() { // Patched by AstralRinth
async function tryOfflineLogin() { // [AR] Feature
loginOfflineModal.value.show()
}
async function offlineLoginFinally() { // Patched by AstralRinth
async function offlineLoginFinally() { // [AR] Feature
const name = playerName.value
if (name.length > 1 && name.length < 20 && name !== '') {
const loggedIn = await offline_login(name).catch(handleError)
@@ -176,12 +175,12 @@ async function offlineLoginFinally() { // Patched by AstralRinth
}
}
function retryOfflineLogin() { // Patched by AstralRinth
function retryOfflineLogin() { // [AR] Feature
loginErrorModal.value.hide()
tryOfflineLogin()
}
function getAccountType(account) { // Patched by AstralRinth
function getAccountType(account) { // [AR] Feature
if (account.access_token != "null" && account.access_token != null && account.access_token != "") {
return License
} else {

View File

@@ -18,11 +18,16 @@ import { cancel_directory_change } from '@/helpers/settings.ts'
import { install } from '@/helpers/profile.js'
import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { applyMigrationFix } from '@/helpers/utils.js'
import { restartApp } from '@/helpers/utils.js'
const errorModal = ref()
const error = ref()
const closable = ref(true)
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 errorType = ref('unknown')
@@ -148,6 +153,30 @@ async function copyToClipboard(text) {
copied.value = false
}, 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>
<template>
@@ -298,10 +327,20 @@ async function copyToClipboard(text) {
<template v-if="copied"> <CheckIcon class="text-green" /> Copied! </template>
<template v-else> <CopyIcon /> Copy debug info </template>
</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>
</div>
<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
class="flex items-center justify-between w-full bg-transparent border-0 px-4 py-3 cursor-pointer"
@click="errorCollapsed = !errorCollapsed"
@@ -313,12 +352,123 @@ async function copyToClipboard(text) {
/>
</button>
<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>
</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>
</div>
</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>
<style>
@@ -333,6 +483,9 @@ async function copyToClipboard(text) {
</style>
<style scoped lang="scss">
@import '../../../../../packages/assets/styles/neon-button.scss';
@import '../../../../../packages/assets/styles/neon-text.scss';
.cta-button {
display: flex;
align-items: center;

View File

@@ -108,7 +108,6 @@ async function testJava() {
testingJava.value = true
testingJavaSuccess.value = await test_jre(
props.modelValue ? props.modelValue.path : '',
1,
props.version,
)
testingJava.value = false

View File

@@ -36,60 +36,6 @@
<span class="circle stopped" />
<span class="running-text"> No instances running </span>
</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>
<transition name="download">
<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 { get_many } from '@/helpers/profile.js'
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 card = ref(null)
@@ -318,101 +241,6 @@ onBeforeUnmount(() => {
</script>
<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 {
display: flex;
flex-direction: row;

View File

@@ -26,6 +26,7 @@ import {
type Version,
} from '@modrinth/utils'
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 ModpackVersionModal from '@/components/ui/ModpackVersionModal.vue'
import dayjs from 'dayjs'
@@ -35,6 +36,11 @@ import type {
Manifest,
} 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 repairConfirmModal = ref()
@@ -447,9 +453,43 @@ const messages = defineMessages({
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>
<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
ref="repairConfirmModal"
:title="formatMessage(messages.repairConfirmTitle)"
@@ -720,6 +760,24 @@ const messages = defineMessages({
</button>
</ButtonStyled>
</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 v-else>
<template v-if="instance.linked_data && instance.linked_data.locked">
@@ -787,3 +845,9 @@ const messages = defineMessages({
</template>
</div>
</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>

View File

@@ -8,6 +8,8 @@ import {
PaintbrushIcon,
GameIcon,
CoffeeIcon,
DownloadIcon,
SpinnerIcon,
} from '@modrinth/assets'
import { TabbedModal } from '@modrinth/ui'
import { computed, ref, watch } from 'vue'
@@ -23,6 +25,23 @@ import { useTheming } from '@/store/state'
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
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()
@@ -138,11 +157,10 @@ function devModeCount() {
{{ formatMessage(developerModeEnabled) }}
</p>
<div class="flex items-center gap-3">
<button
<button
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
:class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }"
@click="devModeCount"
>
@click="devModeCount">
<AstralRinthLogo class="w-6 h-6" />
</button>
<div>
@@ -153,9 +171,80 @@ function devModeCount() {
{{ osVersion }}
</p>
</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>
</template>
</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>
</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>

View File

@@ -30,7 +30,7 @@ watch(
option, you opt out and ads will no longer be shown based on your interests.
</p>
</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" />
</div>
@@ -43,7 +43,7 @@ watch(
longer be collected.
</p>
</div>
<!-- AstralRinth disabled element by default -->
<!-- [AR] Patch. Disabled element by default -->
<Toggle id="opt-out-analytics" v-model="settings.telemetry" :disabled="!settings.telemetry" />
</div>

View File

@@ -36,8 +36,8 @@ export async function get_jre(path) {
// Tests JRE version by running 'java -version' on it.
// Returns true if the version is valid, and matches given (after extraction)
export async function test_jre(path, majorVersion, minorVersion) {
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion, minorVersion })
export async function test_jre(path, majorVersion) {
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion })
}
// Automatically installs specified java version

View File

@@ -2,25 +2,40 @@ import * as THREE from 'three'
import type { Skin, Cape } from '../skins'
import { get_normalized_skin_texture, determineModelType } from '../skins'
import { reactive } from 'vue'
import { setupSkinModel, disposeCaches } from '@modrinth/utils'
import { setupSkinModel, disposeCaches, loadTexture, applyCapeTexture } from '@modrinth/utils'
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 {
forwards: string
backwards: string
}
export interface RawRenderResult {
forwards: Blob
backwards: Blob
}
class BatchSkinRenderer {
private renderer: THREE.WebGLRenderer
private readonly scene: THREE.Scene
private readonly camera: THREE.PerspectiveCamera
private renderer: THREE.WebGLRenderer | null = null
private scene: THREE.Scene | null = null
private camera: THREE.PerspectiveCamera | null = null
private currentModel: THREE.Group | null = null
private readonly width: number
private readonly height: number
constructor(width: number = 360, height: number = 504) {
this.width = width
this.height = height
}
private initializeRenderer(): void {
if (this.renderer) return
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
canvas.width = this.width
canvas.height = this.height
this.renderer = new THREE.WebGLRenderer({
canvas: canvas,
@@ -33,10 +48,10 @@ class BatchSkinRenderer {
this.renderer.toneMapping = THREE.NoToneMapping
this.renderer.toneMappingExposure = 10.0
this.renderer.setClearColor(0x000000, 0)
this.renderer.setSize(width, height)
this.renderer.setSize(this.width, this.height)
this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera(20, width / height, 0.4, 1000)
this.camera = new THREE.PerspectiveCamera(20, this.width / this.height, 0.4, 1000)
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
@@ -50,9 +65,12 @@ class BatchSkinRenderer {
textureUrl: string,
modelUrl: string,
capeUrl?: string,
capeModelUrl?: string,
): Promise<RenderResult> {
await this.setupModel(modelUrl, textureUrl, capeModelUrl, capeUrl)
): Promise<RawRenderResult> {
this.initializeRenderer()
this.clearScene()
await this.setupModel(modelUrl, textureUrl, capeUrl)
const headPart = this.currentModel!.getObjectByName('Head')
let lookAtTarget: [number, number, number]
@@ -77,35 +95,32 @@ class BatchSkinRenderer {
private async renderView(
cameraPosition: [number, number, number],
lookAtPosition: [number, number, number],
): Promise<string> {
): Promise<Blob> {
if (!this.camera || !this.renderer || !this.scene) {
throw new Error('Renderer not initialized')
}
this.camera.position.set(...cameraPosition)
this.camera.lookAt(...lookAtPosition)
this.renderer.render(this.scene, this.camera)
return new Promise<string>((resolve, reject) => {
this.renderer.domElement.toBlob((blob) => {
if (blob) {
const url = URL.createObjectURL(blob)
resolve(url)
} else {
reject(new Error('Failed to create blob from canvas'))
}
}, 'image/png')
})
const dataUrl = this.renderer.domElement.toDataURL('image/webp', 0.9)
const response = await fetch(dataUrl)
return await response.blob()
}
private async setupModel(
modelUrl: string,
textureUrl: string,
capeModelUrl?: string,
capeUrl?: string,
): Promise<void> {
if (this.currentModel) {
this.scene.remove(this.currentModel)
private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
if (!this.scene) {
throw new Error('Renderer not initialized')
}
const { model } = await setupSkinModel(modelUrl, textureUrl, capeModelUrl, capeUrl)
const { model } = await setupSkinModel(modelUrl, textureUrl)
if (capeUrl) {
const capeTexture = await loadTexture(capeUrl)
applyCapeTexture(model, capeTexture)
}
const group = new THREE.Group()
group.add(model)
@@ -116,8 +131,39 @@ class BatchSkinRenderer {
this.currentModel = group
}
private clearScene(): void {
if (!this.scene) return
while (this.scene.children.length > 0) {
const child = this.scene.children[0]
this.scene.remove(child)
if (child instanceof THREE.Mesh) {
if (child.geometry) child.geometry.dispose()
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach((material) => material.dispose())
} else {
child.material.dispose()
}
}
}
}
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
directionalLight.castShadow = true
directionalLight.position.set(2, 4, 3)
this.scene.add(ambientLight)
this.scene.add(directionalLight)
this.currentModel = null
}
public dispose(): void {
this.renderer.dispose()
if (this.renderer) {
this.renderer.dispose()
}
disposeCaches()
}
}
@@ -133,10 +179,25 @@ function getModelUrlForVariant(variant: string): string {
}
}
export const map = reactive(new Map<string, RenderResult>())
export const headMap = reactive(new Map<string, string>())
export const skinBlobUrlMap = reactive(new Map<string, RenderResult>())
export const headBlobUrlMap = reactive(new Map<string, string>())
const DEBUG_MODE = false
let sharedRenderer: BatchSkinRenderer | null = null
function getSharedRenderer(): BatchSkinRenderer {
if (!sharedRenderer) {
sharedRenderer = new BatchSkinRenderer()
}
return sharedRenderer
}
export function disposeSharedRenderer(): void {
if (sharedRenderer) {
sharedRenderer.dispose()
sharedRenderer = null
}
}
export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
const validKeys = new Set<string>()
const validHeadKeys = new Set<string>()
@@ -150,7 +211,7 @@ export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
try {
await skinPreviewStorage.cleanupInvalidKeys(validKeys)
await skinPreviewStorage.cleanupInvalidKeys(validHeadKeys)
await headStorage.cleanupInvalidKeys(validHeadKeys)
} catch (error) {
console.warn('Failed to cleanup unused skin previews:', error)
}
@@ -229,13 +290,17 @@ export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64)
outputCtx.drawImage(hatCanvas, 0, 0, 8, 8, 0, 0, size, size)
}
outputCanvas.toBlob((blob) => {
if (blob) {
resolve(blob)
} else {
reject(new Error('Failed to create blob from canvas'))
}
}, 'image/png')
outputCanvas.toBlob(
(blob) => {
if (blob) {
resolve(blob)
} else {
reject(new Error('Failed to create blob from canvas'))
}
},
'image/webp',
0.9,
)
} catch (error) {
reject(error)
}
@@ -252,35 +317,24 @@ export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64)
async function generateHeadRender(skin: Skin): Promise<string> {
const headKey = `${skin.texture_key}-head`
if (headMap.has(headKey)) {
if (headBlobUrlMap.has(headKey)) {
if (DEBUG_MODE) {
const url = headMap.get(headKey)!
const url = headBlobUrlMap.get(headKey)!
URL.revokeObjectURL(url)
headMap.delete(headKey)
headBlobUrlMap.delete(headKey)
} else {
return headMap.get(headKey)!
return headBlobUrlMap.get(headKey)!
}
}
try {
const cached = await skinPreviewStorage.retrieve(headKey)
if (cached && typeof cached === 'string') {
headMap.set(headKey, cached)
return cached
}
} catch (error) {
console.warn('Failed to retrieve cached head render:', error)
}
const skinUrl = await get_normalized_skin_texture(skin)
const headBlob = await generatePlayerHeadBlob(skinUrl, 64)
const headUrl = URL.createObjectURL(headBlob)
headMap.set(headKey, headUrl)
headBlobUrlMap.set(headKey, headUrl)
try {
// @ts-expect-error - skinPreviewStorage.store expects a RenderResult, but we are storing a string url.
await skinPreviewStorage.store(headKey, headUrl)
await headStorage.store(headKey, headBlob)
} catch (error) {
console.warn('Failed to store head render in persistent storage:', error)
}
@@ -293,30 +347,49 @@ export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
}
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
const renderer = new BatchSkinRenderer()
try {
const skinKeys = skins.map(
(skin) => `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`,
)
const headKeys = skins.map((skin) => `${skin.texture_key}-head`)
const [cachedSkinPreviews, cachedHeadPreviews] = await Promise.all([
skinPreviewStorage.batchRetrieve(skinKeys),
headStorage.batchRetrieve(headKeys),
])
for (let i = 0; i < skins.length; i++) {
const skinKey = skinKeys[i]
const headKey = headKeys[i]
const rawCached = cachedSkinPreviews[skinKey]
if (rawCached) {
const cached: RenderResult = {
forwards: URL.createObjectURL(rawCached.forwards),
backwards: URL.createObjectURL(rawCached.backwards),
}
skinBlobUrlMap.set(skinKey, cached)
}
const cachedHead = cachedHeadPreviews[headKey]
if (cachedHead) {
headBlobUrlMap.set(headKey, URL.createObjectURL(cachedHead))
}
}
for (const skin of skins) {
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
if (map.has(key)) {
if (skinBlobUrlMap.has(key)) {
if (DEBUG_MODE) {
const result = map.get(key)!
const result = skinBlobUrlMap.get(key)!
URL.revokeObjectURL(result.forwards)
URL.revokeObjectURL(result.backwards)
map.delete(key)
skinBlobUrlMap.delete(key)
} else continue
}
try {
const cached = await skinPreviewStorage.retrieve(key)
if (cached) {
map.set(key, cached)
continue
}
} catch (error) {
console.warn('Failed to retrieve cached skin preview:', error)
}
const renderer = getSharedRenderer()
let variant = skin.variant
if (variant === 'UNKNOWN') {
@@ -330,25 +403,35 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
const modelUrl = getModelUrlForVariant(variant)
const cape: Cape | undefined = capes.find((_cape) => _cape.id === skin.cape_id)
const renderResult = await renderer.renderSkin(
const rawRenderResult = await renderer.renderSkin(
await get_normalized_skin_texture(skin),
modelUrl,
cape?.texture,
CapeModel,
)
map.set(key, renderResult)
const renderResult: RenderResult = {
forwards: URL.createObjectURL(rawRenderResult.forwards),
backwards: URL.createObjectURL(rawRenderResult.backwards),
}
skinBlobUrlMap.set(key, renderResult)
try {
await skinPreviewStorage.store(key, renderResult)
await skinPreviewStorage.store(key, rawRenderResult)
} catch (error) {
console.warn('Failed to store skin preview in persistent storage:', error)
}
await generateHeadRender(skin)
const headKey = `${skin.texture_key}-head`
if (!headBlobUrlMap.has(headKey)) {
await generateHeadRender(skin)
}
}
} finally {
renderer.dispose()
disposeSharedRenderer()
await cleanupUnusedPreviews(skins)
await skinPreviewStorage.debugCalculateStorage()
await headStorage.debugCalculateStorage()
}
}

View 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()

View File

@@ -1,4 +1,4 @@
import type { RenderResult } from '../rendering/batch-skin-renderer'
import type { RawRenderResult } from '../rendering/batch-skin-renderer'
interface StoredPreview {
forwards: Blob
@@ -30,18 +30,15 @@ export class SkinPreviewStorage {
})
}
async store(key: string, result: RenderResult): Promise<void> {
async store(key: string, result: RawRenderResult): Promise<void> {
if (!this.db) await this.init()
const forwardsBlob = await fetch(result.forwards).then((r) => r.blob())
const backwardsBlob = await fetch(result.backwards).then((r) => r.blob())
const transaction = this.db!.transaction(['previews'], 'readwrite')
const store = transaction.objectStore('previews')
const storedPreview: StoredPreview = {
forwards: forwardsBlob,
backwards: backwardsBlob,
forwards: result.forwards,
backwards: result.backwards,
timestamp: Date.now(),
}
@@ -53,7 +50,7 @@ export class SkinPreviewStorage {
})
}
async retrieve(key: string): Promise<RenderResult | null> {
async retrieve(key: string): Promise<RawRenderResult | null> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['previews'], 'readonly')
@@ -70,14 +67,56 @@ export class SkinPreviewStorage {
return
}
const forwards = URL.createObjectURL(result.forwards)
const backwards = URL.createObjectURL(result.backwards)
resolve({ forwards, backwards })
resolve({ forwards: result.forwards, backwards: result.backwards })
}
request.onerror = () => reject(request.error)
})
}
async batchRetrieve(keys: string[]): Promise<Record<string, RawRenderResult | null>> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['previews'], 'readonly')
const store = transaction.objectStore('previews')
const results: Record<string, RawRenderResult | null> = {}
return new Promise((resolve, _reject) => {
let completedRequests = 0
if (keys.length === 0) {
resolve(results)
return
}
for (const key of keys) {
const request = store.get(key)
request.onsuccess = () => {
const result = request.result as StoredPreview | undefined
if (result) {
results[key] = { forwards: result.forwards, backwards: result.backwards }
} else {
results[key] = null
}
completedRequests++
if (completedRequests === keys.length) {
resolve(results)
}
}
request.onerror = () => {
results[key] = null
completedRequests++
if (completedRequests === keys.length) {
resolve(results)
}
}
}
})
}
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
if (!this.db) await this.init()
@@ -113,6 +152,67 @@ export class SkinPreviewStorage {
request.onerror = () => reject(request.error)
})
}
async debugCalculateStorage(): Promise<void> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['previews'], 'readonly')
const store = transaction.objectStore('previews')
let totalSize = 0
let count = 0
const entries: Array<{ key: string; size: number }> = []
return new Promise((resolve, reject) => {
const request = store.openCursor()
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
if (cursor) {
const key = cursor.primaryKey as string
const value = cursor.value as StoredPreview
const entrySize = value.forwards.size + value.backwards.size
totalSize += entrySize
count++
entries.push({
key,
size: entrySize,
})
cursor.continue()
} else {
console.group('🗄️ Skin Preview Storage Debug Info')
console.log(`Total entries: ${count}`)
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
console.log(
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
)
if (entries.length > 0) {
const sortedEntries = entries.sort((a, b) => b.size - a.size)
console.log(
'Largest entry:',
sortedEntries[0].key,
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
)
console.log(
'Smallest entry:',
sortedEntries[sortedEntries.length - 1].key,
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
)
}
console.groupEnd()
resolve()
}
}
request.onerror = () => reject(request.error)
})
}
}
export const skinPreviewStorage = new SkinPreviewStorage()

View File

@@ -1,6 +1,6 @@
import { ref } from 'vue'
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 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 osList = ['macos', 'windows', 'linux']
const macExtensionList = ['.app', '.dmg']
const macExtensionList = ['.dmg', '.pkg']
const windowsExtensionList = ['.exe', '.msi']
const blacklistPrefixes = [
@@ -52,7 +52,7 @@ export async function getRemote(isDownloadState) {
installState.value = true;
const builds = remoteData.assets;
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;
}

View File

@@ -10,9 +10,20 @@ export async function getOS() {
return await invoke('plugin:utils|get_os')
}
export async function getArtifact(downloadurl, filename, ostype, autoupdatesupported) {
// [AR] Feature
export async function initUpdateLauncher(downloadurl, filename, ostype, autoupdatesupported) {
console.log('Downloading build', downloadurl, filename, ostype, autoupdatesupported)
return await invoke('plugin:utils|get_artifact', { downloadurl, filename, ostype, autoupdatesupported })
return await invoke('plugin:utils|init_update_launcher', { downloadurl, filename, ostype, autoupdatesupported })
}
// [AR] Patch fix
export async function applyMigrationFix(eol) {
return await invoke('plugin:utils|apply_migration_fix', { eol })
}
// [AR] Feature
export async function initAuthlibPatching(minecraftversion, ismojang) {
return await invoke('plugin:utils|init_authlib_patching', { minecraftversion, ismojang })
}
export async function openPath(path) {

View File

@@ -38,7 +38,7 @@ import {
import { get as getSettings } from '@/helpers/settings.ts'
import { get_default_user, login as login_flow, users } from '@/helpers/auth'
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
import { generateSkinPreviews, map } from '@/helpers/rendering/batch-skin-renderer.ts'
import { generateSkinPreviews, skinBlobUrlMap } from '@/helpers/rendering/batch-skin-renderer.ts'
import { handleSevereError } from '@/store/error'
import { trackEvent } from '@/helpers/analytics'
import type AccountsCard from '@/components/ui/AccountsCard.vue'
@@ -215,7 +215,7 @@ async function loadCurrentUser() {
function getBakedSkinTextures(skin: Skin): RenderResult | undefined {
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
return map.get(key)
return skinBlobUrlMap.get(key)
}
async function login() {

View File

@@ -483,7 +483,7 @@ onUnmounted(() => {
display: flex;
flex-direction: column;
gap: 1rem;
height: calc(100vh - 11rem);
height: 100vh;
}
.button-row {

View File

@@ -218,7 +218,9 @@ fn main() {
"utils",
InlinedPlugin::new()
.commands(&[
"get_artifact",
"init_authlib_patching",
"apply_migration_fix",
"init_update_launcher",
"get_os",
"should_disable_mouseover",
"highlight_in_folder",

View File

@@ -10,12 +10,15 @@ use crate::api::{Result, TheseusSerializableError};
use dashmap::DashMap;
use std::path::{Path, PathBuf};
use theseus::prelude::canonicalize;
use theseus::util::utils;
use url::Url;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("utils")
.invoke_handler(tauri::generate_handler![
get_artifact,
init_authlib_patching,
apply_migration_fix,
init_update_launcher,
get_os,
should_disable_mouseover,
highlight_in_folder,
@@ -27,9 +30,39 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
.build()
}
/// [AR] Feature
#[tauri::command]
pub async fn get_artifact(downloadurl: &str, filename: &str, ostype: &str, autoupdatesupported: bool) -> Result<()> {
theseus::download::init_download(downloadurl, filename, ostype, autoupdatesupported).await;
pub async fn init_authlib_patching(
minecraftversion: &str,
ismojang: bool,
) -> Result<bool> {
let result =
utils::init_authlib_patching(minecraftversion, ismojang).await?;
Ok(result)
}
/// [AR] Patch fix
#[tauri::command]
pub async fn apply_migration_fix(eol: &str) -> Result<bool> {
let result = utils::apply_migration_fix(eol).await?;
Ok(result)
}
/// [AR] Feature
#[tauri::command]
pub async fn init_update_launcher(
downloadurl: &str,
filename: &str,
ostype: &str,
autoupdatesupported: bool,
) -> Result<()> {
let _ = utils::init_update_launcher(
downloadurl,
filename,
ostype,
autoupdatesupported,
)
.await;
Ok(())
}

View File

@@ -157,7 +157,7 @@ fn main() {
*/
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();

View File

@@ -41,7 +41,7 @@
]
},
"productName": "AstralRinth App",
"version": "0.10.302",
"version": "0.10.304",
"mainBinaryName": "AstralRinth App",
"identifier": "AstralRinthApp",
"plugins": {

View File

@@ -1,5 +1,4 @@
FROM rust:1.88.0 AS build
ENV PKG_CONFIG_ALLOW_CROSS=1
WORKDIR /usr/src/daedalus
COPY . .
@@ -10,11 +9,8 @@ FROM debian:bookworm-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates openssl \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN update-ca-certificates
COPY --from=build /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
WORKDIR /daedalus_client

View File

@@ -102,7 +102,7 @@ export class ModrinthServer {
try {
const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, {
override: auth,
retry: false,
retry: 1, // Reduce retries for optional resources
});
if (fileData instanceof Blob && import.meta.client) {
@@ -124,8 +124,14 @@ export class ModrinthServer {
return dataURL;
}
} catch (error) {
if (error instanceof ModrinthServerError && error.statusCode === 404) {
if (iconUrl) {
if (error instanceof ModrinthServerError) {
if (error.statusCode && error.statusCode >= 500) {
console.debug("Service unavailable, skipping icon processing");
sharedImage.value = undefined;
return undefined;
}
if (error.statusCode === 404 && iconUrl) {
try {
const response = await fetch(iconUrl);
if (!response.ok) throw new Error("Failed to fetch icon");
@@ -187,6 +193,45 @@ export class ModrinthServer {
return undefined;
}
async testNodeReachability(): Promise<boolean> {
if (!this.general?.datacenter) {
console.warn("No datacenter info available for ping test");
return false;
}
const datacenter = this.general.datacenter;
const wsUrl = `wss://${datacenter}.nodes.modrinth.com/pingtest`;
try {
return await new Promise((resolve) => {
const socket = new WebSocket(wsUrl);
const timeout = setTimeout(() => {
socket.close();
resolve(false);
}, 5000);
socket.onopen = () => {
clearTimeout(timeout);
socket.send(performance.now().toString());
};
socket.onmessage = () => {
clearTimeout(timeout);
socket.close();
resolve(true);
};
socket.onerror = () => {
clearTimeout(timeout);
resolve(false);
};
});
} catch (error) {
console.error(`Failed to ping node ${wsUrl}:`, error);
return false;
}
}
async refresh(
modules: ModuleName[] = [],
options?: {
@@ -200,6 +245,8 @@ export class ModrinthServer {
: (["general", "content", "backups", "network", "startup", "ws", "fs"] as ModuleName[]);
for (const module of modulesToRefresh) {
this.errors[module] = undefined;
try {
switch (module) {
case "general": {
@@ -250,7 +297,7 @@ export class ModrinthServer {
continue;
}
if (error.statusCode === 503) {
if (error.statusCode && error.statusCode >= 500) {
console.debug(`Temporary ${module} unavailable:`, error.message);
continue;
}

View File

@@ -22,26 +22,49 @@ export class FSModule extends ServerModule {
this.opsQueuedForModification = [];
}
private async retryWithAuth<T>(requestFn: () => Promise<T>): Promise<T> {
private async retryWithAuth<T>(
requestFn: () => Promise<T>,
ignoreFailure: boolean = false,
): Promise<T> {
try {
return await requestFn();
} catch (error) {
if (error instanceof ModrinthServerError && error.statusCode === 401) {
console.debug("Auth failed, refreshing JWT and retrying");
await this.fetch(); // Refresh auth
return await requestFn();
}
const available = await this.server.testNodeReachability();
if (!available && !ignoreFailure) {
this.server.moduleErrors.general = {
error: new ModrinthServerError(
"Unable to reach node. FS operation failed and subsequent ping test failed.",
500,
error as Error,
"fs",
),
timestamp: Date.now(),
};
}
throw error;
}
}
listDirContents(path: string, page: number, pageSize: number): Promise<DirectoryResponse> {
listDirContents(
path: string,
page: number,
pageSize: number,
ignoreFailure: boolean = false,
): Promise<DirectoryResponse> {
return this.retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path);
return await useServersFetch(`/list?path=${encodedPath}&page=${page}&page_size=${pageSize}`, {
override: this.auth,
retry: false,
});
});
}, ignoreFailure);
}
createFileOrFolder(path: string, type: "file" | "directory"): Promise<void> {
@@ -150,7 +173,7 @@ export class FSModule extends ServerModule {
});
}
downloadFile(path: string, raw?: boolean): Promise<any> {
downloadFile(path: string, raw: boolean = false, ignoreFailure: boolean = false): Promise<any> {
return this.retryWithAuth(async () => {
const encodedPath = encodeURIComponent(path);
const fileData = await useServersFetch(`/download?path=${encodedPath}`, {
@@ -161,7 +184,7 @@ export class FSModule extends ServerModule {
return raw ? fileData : await fileData.text();
}
return fileData;
});
}, ignoreFailure);
}
extractFile(

View File

@@ -46,13 +46,18 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
data.image = (await this.server.processImage(data.project?.icon_url)) ?? undefined;
}
const motd = await this.getMotd();
if (motd === "A Minecraft Server") {
await this.setMotd(
`§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`,
);
try {
const motd = await this.getMotd();
if (motd === "A Minecraft Server") {
await this.setMotd(
`§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`,
);
}
data.motd = motd;
} catch {
console.error("[Modrinth Servers] [General] Failed to fetch MOTD.");
data.motd = undefined;
}
data.motd = motd;
// Copy data to this module
Object.assign(this, data);
@@ -178,7 +183,7 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
async getMotd(): Promise<string | undefined> {
try {
const props = await this.server.fs.downloadFile("/server.properties");
const props = await this.server.fs.downloadFile("/server.properties", false, true);
if (props) {
const lines = props.split("\n");
for (const line of lines) {

View File

@@ -42,6 +42,23 @@ export async function useServersFetch<T>(
retry = method === "GET" ? 3 : 0,
} = options;
const circuitBreakerKey = `${module || "default"}_${path}`;
const failureCount = useState<number>(`fetch_failures_${circuitBreakerKey}`, () => 0);
const lastFailureTime = useState<number>(`last_failure_${circuitBreakerKey}`, () => 0);
const now = Date.now();
if (failureCount.value >= 3 && now - lastFailureTime.value < 30000) {
const error = new ModrinthServersFetchError(
"[Modrinth Servers] Circuit breaker open - too many recent failures",
503,
);
throw new ModrinthServerError("Service temporarily unavailable", 503, error, module);
}
if (now - lastFailureTime.value > 30000) {
failureCount.value = 0;
}
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
/\/$/,
"",
@@ -69,6 +86,7 @@ export async function useServersFetch<T>(
const headers: Record<string, string> = {
"User-Agent": "Modrinth/1.0 (https://modrinth.com)",
"X-Archon-Request": "true",
Vary: "Accept, Origin",
};
@@ -98,6 +116,7 @@ export async function useServersFetch<T>(
timeout: 10000,
});
failureCount.value = 0;
return response;
} catch (error) {
lastError = error as Error;
@@ -107,6 +126,11 @@ export async function useServersFetch<T>(
const statusCode = error.response?.status;
const statusText = error.response?.statusText || "Unknown error";
if (statusCode && statusCode >= 500) {
failureCount.value++;
lastFailureTime.value = now;
}
let v1Error: V1ErrorInfo | undefined;
if (error.data?.error && error.data?.description) {
v1Error = {
@@ -134,9 +158,11 @@ export async function useServersFetch<T>(
? errorMessages[statusCode]
: `HTTP Error: ${statusCode || "unknown"} ${statusText}`;
const isRetryable = statusCode ? [408, 429, 500, 502, 504].includes(statusCode) : true;
const isRetryable = statusCode ? [408, 429].includes(statusCode) : false;
const is5xxRetryable =
statusCode && statusCode >= 500 && statusCode < 600 && method === "GET" && attempts === 1;
if (!isRetryable || attempts >= maxAttempts) {
if (!(isRetryable || is5xxRetryable) || attempts >= maxAttempts) {
console.error("Fetch error:", error);
const fetchError = new ModrinthServersFetchError(
@@ -147,7 +173,8 @@ export async function useServersFetch<T>(
throw new ModrinthServerError(error.message, statusCode, fetchError, module, v1Error);
}
const delay = Math.min(1000 * Math.pow(2, attempts - 1) + Math.random() * 1000, 10000);
const baseDelay = statusCode && statusCode >= 500 ? 5000 : 1000;
const delay = Math.min(baseDelay * Math.pow(2, attempts - 1) + Math.random() * 1000, 15000);
console.warn(`Retrying request in ${delay}ms (attempt ${attempts}/${maxAttempts - 1})`);
await new Promise((resolve) => setTimeout(resolve, delay));
continue;

View File

@@ -1,9 +1,10 @@
<script setup lang="ts">
import { ButtonStyled } from "@modrinth/ui";
import { Avatar, ButtonStyled } from "@modrinth/ui";
import { RssIcon, GitGraphIcon } from "@modrinth/assets";
import dayjs from "dayjs";
import { articles as rawArticles } from "@modrinth/blog";
import { computed } from "vue";
import type { User } from "@modrinth/utils";
import ShareArticleButtons from "~/components/ui/ShareArticleButtons.vue";
import NewsletterButton from "~/components/ui/NewsletterButton.vue";
@@ -20,7 +21,21 @@ if (!rawArticle) {
});
}
const html = await rawArticle.html();
const authorsUrl = `users?ids=${JSON.stringify(rawArticle.authors)}`;
const [authors, html] = await Promise.all([
rawArticle.authors
? useAsyncData(authorsUrl, () => useBaseFetch(authorsUrl)).then((data) => {
const users = data.data as Ref<User[]>;
users.value.sort((a, b) => {
return rawArticle.authors.indexOf(a.id) - rawArticle.authors.indexOf(b.id);
});
return users;
})
: Promise.resolve(),
rawArticle.html(),
]);
const article = computed(() => ({
...rawArticle,
@@ -34,6 +49,8 @@ const article = computed(() => ({
html,
}));
const authorCount = computed(() => authors?.value?.length ?? 0);
const articleTitle = computed(() => article.value.title);
const articleUrl = computed(() => `https://modrinth.com/news/article/${route.params.slug}`);
@@ -83,9 +100,35 @@ useSeoMeta({
<article class="mt-6 flex flex-col gap-4 px-6">
<h2 class="m-0 text-2xl font-extrabold leading-tight sm:text-4xl">{{ article.title }}</h2>
<p class="m-0 text-base leading-tight sm:text-lg">{{ article.summary }}</p>
<div class="mt-auto text-sm text-secondary sm:text-base">
Posted on {{ dayjsDate.format("MMMM D, YYYY") }}
<div class="mt-auto flex flex-wrap items-center gap-1 text-sm text-secondary sm:text-base">
<template v-for="(author, index) in authors" :key="`author-${author.id}`">
<span v-if="authorCount - 1 === index && authorCount > 1">and</span>
<span class="flex items-center">
<nuxt-link
:to="`/user/${author.id}`"
class="inline-flex items-center gap-1 font-semibold hover:underline hover:brightness-[--hover-brightness]"
>
<Avatar :src="author.avatar_url" circle size="24px" />
{{ author.username }}
</nuxt-link>
<span v-if="(authors?.length ?? 0) > 2 && index !== authorCount - 1">,</span>
</span>
</template>
<template v-if="!authors || authorCount === 0">
<nuxt-link
to="/organization/modrinth"
class="inline-flex items-center gap-1 font-semibold hover:underline hover:brightness-[--hover-brightness]"
>
<Avatar src="https://cdn-raw.modrinth.com/modrinth-icon-96.webp" size="24px" />
Modrinth Team
</nuxt-link>
</template>
<span class="hidden md:block"></span>
<span class="hidden md:block"> {{ dayjsDate.format("MMMM D, YYYY") }}</span>
</div>
<span class="text-sm text-secondary sm:text-base md:hidden">
Posted on {{ dayjsDate.format("MMMM D, YYYY") }}</span
>
<ShareArticleButtons :title="article.title" :url="articleUrl" />
<img
:src="article.thumbnail"

View File

@@ -719,31 +719,32 @@ async function fetchCapacityStatuses(customProduct = null) {
product.metadata.ram < min.metadata.ram ? product : min,
),
];
const capacityChecks = productsToCheck.map((product) =>
useServersFetch("stock", {
method: "POST",
body: {
cpu: product.metadata.cpu,
memory_mb: product.metadata.ram,
swap_mb: product.metadata.swap,
storage_mb: product.metadata.storage,
},
bypassAuth: true,
}),
);
const results = await Promise.all(capacityChecks);
const capacityChecks = [];
for (const product of productsToCheck) {
capacityChecks.push(
useServersFetch("stock", {
method: "POST",
body: {
cpu: product.metadata.cpu,
memory_mb: product.metadata.ram,
swap_mb: product.metadata.swap,
storage_mb: product.metadata.storage,
},
bypassAuth: true,
}),
);
}
if (customProduct?.metadata) {
return {
custom: results[0],
custom: await capacityChecks[0],
};
} else {
return {
small: results[0],
medium: results[1],
large: results[2],
custom: results[3],
small: await capacityChecks[0],
medium: await capacityChecks[1],
large: await capacityChecks[2],
custom: await capacityChecks[3],
};
}
} catch (error) {
@@ -760,6 +761,11 @@ async function fetchCapacityStatuses(customProduct = null) {
const { data: capacityStatuses, refresh: refreshCapacity } = await useAsyncData(
"ServerCapacityAll",
fetchCapacityStatuses,
{
getCachedData() {
return null; // Dont cache stock data.
},
},
);
const isSmallAtCapacity = computed(() => capacityStatuses.value?.small?.available === 0);

View File

@@ -55,7 +55,7 @@
/>
</div>
<div
v-else-if="server.moduleErrors?.general?.error.statusCode === 503"
v-else-if="server.moduleErrors?.general?.error || !nodeAccessible"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
>
<ErrorInformationCard
@@ -68,22 +68,22 @@
<template #description>
<div class="text-md space-y-4">
<p class="leading-[170%] text-secondary">
Your server's node, where your Modrinth Server is physically hosted, is experiencing
issues. We are working with our datacenter to resolve the issue as quickly as possible.
Your server's node, where your Modrinth Server is physically hosted, is not accessible
at the moment. We are working to resolve the issue as quickly as possible.
</p>
<p class="leading-[170%] text-secondary">
Your data is safe and will not be lost, and your server will be back online as soon as
the issue is resolved.
</p>
<p class="leading-[170%] text-secondary">
For updates, please join the Modrinth Discord or contact Modrinth Support via the chat
If reloading does not work initially, please contact Modrinth Support via the chat
bubble in the bottom right corner and we'll be happy to help.
</p>
</div>
</template>
</ErrorInformationCard>
</div>
<div
<!-- <div
v-else-if="server.moduleErrors?.general?.error"
class="flex min-h-[calc(100vh-4rem)] items-center justify-center text-contrast"
>
@@ -96,19 +96,14 @@
>
<template #description>
<div class="space-y-4">
<div class="text-center text-secondary">
{{
formattedTime == "00" ? "Reconnecting..." : `Retrying in ${formattedTime} seconds...`
}}
</div>
<p class="text-lg text-secondary">
Something went wrong, and we couldn't connect to your server. This is likely due to a
temporary network issue. You'll be reconnected automatically.
temporary network issue.
</p>
</div>
</template>
</ErrorInformationCard>
</div>
</div> -->
<!-- SERVER START -->
<div
v-else-if="serverData"
@@ -355,7 +350,7 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch, type Reactive } from "vue";
import { ref, computed, onMounted, onUnmounted, type Reactive } from "vue";
import {
SettingsIcon,
CopyIcon,
@@ -371,15 +366,15 @@ import DOMPurify from "dompurify";
import { ButtonStyled, ErrorInformationCard, ServerNotice } from "@modrinth/ui";
import { Intercom, shutdown } from "@intercom/messenger-js-sdk";
import type { MessageDescriptor } from "@vintl/vintl";
import type {
ServerState,
Stats,
WSEvent,
WSInstallationResultEvent,
Backup,
PowerAction,
import {
type ServerState,
type Stats,
type WSEvent,
type WSInstallationResultEvent,
type Backup,
type PowerAction,
} from "@modrinth/utils";
import { reloadNuxtApp, navigateTo } from "#app";
import { reloadNuxtApp } from "#app";
import { useModrinthServersConsole } from "~/store/console.ts";
import { useServersFetch } from "~/composables/servers/servers-fetch.ts";
import { ModrinthServer, useModrinthServers } from "~/composables/servers/modrinth-servers.ts";
@@ -392,7 +387,6 @@ const socket = ref<WebSocket | null>(null);
const isReconnecting = ref(false);
const isLoading = ref(true);
const reconnectInterval = ref<ReturnType<typeof setInterval> | null>(null);
const isFirstMount = ref(true);
const isMounted = ref(true);
const flags = useFeatureFlags();
@@ -422,18 +416,6 @@ const loadModulesPromise = Promise.resolve().then(() => {
provide("modulesLoaded", loadModulesPromise);
watch(
() => [server.moduleErrors?.general, server.moduleErrors?.ws],
([generalError, wsError]) => {
if (server.general?.status === "suspended") return;
const error = generalError?.error || wsError?.error;
if (error && error.statusCode !== 403) {
startPolling();
}
},
);
const errorTitle = ref("Error");
const errorMessage = ref("An unexpected error occurred.");
const errorLog = ref("");
@@ -697,7 +679,6 @@ const startUptimeUpdates = () => {
const stopUptimeUpdates = () => {
if (uptimeIntervalId) {
clearInterval(uptimeIntervalId);
pollingIntervalId = null;
}
};
@@ -836,8 +817,6 @@ const handleInstallationResult = async (data: WSInstallationResultEvent) => {
case "ok": {
if (!serverData.value) break;
stopPolling();
try {
await new Promise((resolve) => setTimeout(resolve, 2000));
@@ -992,14 +971,6 @@ const notifyError = (title: string, text: string) => {
});
};
let pollingIntervalId: ReturnType<typeof setInterval> | null = null;
const countdown = ref(15);
const formattedTime = computed(() => {
const seconds = countdown.value % 60;
return `${seconds.toString().padStart(2, "0")}`;
});
export type BackupInProgressReason = {
type: string;
tooltip: MessageDescriptor;
@@ -1035,54 +1006,6 @@ const backupInProgress = computed(() => {
return undefined;
});
const stopPolling = () => {
if (pollingIntervalId) {
clearTimeout(pollingIntervalId);
pollingIntervalId = null;
}
};
const startPolling = () => {
stopPolling();
let retryCount = 0;
const maxRetries = 10;
const poll = async () => {
try {
await server.refresh(["general", "ws"]);
if (!server.moduleErrors?.general?.error) {
stopPolling();
connectWebSocket();
return;
}
retryCount++;
if (retryCount >= maxRetries) {
console.error("Max retries reached, stopping polling");
stopPolling();
return;
}
// Exponential backoff: 3s, 6s, 12s, 24s, etc.
const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000);
pollingIntervalId = setTimeout(poll, delay);
} catch (error) {
console.error("Polling failed:", error);
retryCount++;
if (retryCount < maxRetries) {
const delay = Math.min(3000 * Math.pow(2, retryCount - 1), 60000);
pollingIntervalId = setTimeout(poll, delay);
}
}
};
poll();
};
const nodeUnavailableDetails = computed(() => [
{
label: "Server ID",
@@ -1091,9 +1014,16 @@ const nodeUnavailableDetails = computed(() => [
},
{
label: "Node",
value: server.general?.datacenter ?? "Unknown! Please contact support!",
value: server.general?.datacenter ?? "Unknown",
type: "inline" as const,
},
{
label: "Error message",
value: nodeAccessible.value
? server.moduleErrors?.general?.error.message ?? "Unknown"
: "Unable to reach node. Ping test failed.",
type: "block" as const,
},
]);
const suspendedDescription = computed(() => {
@@ -1160,16 +1090,10 @@ const generalErrorAction = computed(() => ({
}));
const nodeUnavailableAction = computed(() => ({
label: "Join Modrinth Discord",
onClick: () => navigateTo("https://discord.modrinth.com", { external: true }),
color: "standard" as const,
}));
const connectionLostAction = computed(() => ({
label: "Reload",
onClick: () => reloadNuxtApp(),
color: "brand" as const,
disabled: formattedTime.value !== "00",
disabled: false,
}));
const copyServerDebugInfo = () => {
@@ -1193,7 +1117,6 @@ const cleanup = () => {
shutdown();
stopPolling();
stopUptimeUpdates();
if (reconnectInterval.value) {
clearInterval(reconnectInterval.value);
@@ -1236,16 +1159,31 @@ async function dismissNotice(noticeId: number) {
await server.refresh(["general"]);
}
const nodeAccessible = ref(true);
onMounted(() => {
isMounted.value = true;
if (server.general?.status === "suspended") {
isLoading.value = false;
return;
}
server
.testNodeReachability()
.then((result) => {
nodeAccessible.value = result;
if (!nodeAccessible.value) {
isLoading.value = false;
}
})
.catch((err) => {
console.error("Error testing node reachability:", err);
nodeAccessible.value = false;
isLoading.value = false;
});
if (server.moduleErrors.general?.error) {
if (!server.moduleErrors.general?.error?.message?.includes("Forbidden")) {
startPolling();
}
isLoading.value = false;
} else {
connectWebSocket();
}
@@ -1297,21 +1235,6 @@ onUnmounted(() => {
cleanup();
});
watch(
() => serverData.value?.status,
(newStatus, oldStatus) => {
if (isFirstMount.value) {
isFirstMount.value = false;
return;
}
if (newStatus === "installing" && oldStatus !== "installing") {
countdown.value = 15;
startPolling();
}
},
);
definePageMeta({
middleware: "auth",
});

View File

@@ -16,7 +16,7 @@
},
{
"title": "A Pride Month Success: Over $8,400 Raised for The Trevor Project!",
"summary": "A reflection on our Pride Month fundraiser campaign, which raised thousands for LGBTQ+ youth.",
"summary": "Reflecting on our Pride Month fundraiser campaign for LGBTQ+ youth.",
"thumbnail": "https://modrinth.com/news/article/pride-campaign-2025/thumbnail.webp",
"date": "2025-07-01T18:00:00.000Z",
"link": "https://modrinth.com/news/article/pride-campaign-2025"
@@ -114,14 +114,14 @@
},
{
"title": "Creators can now make money on Modrinth!",
"summary": "Yes, you read the title correctly: Modrinth's creator monetization program, also known as payouts, is now in an open beta phase. Read on for more information!",
"summary": "Introducing the Creator Monetization Program allowing creators to earn revenue from their projects.",
"thumbnail": "https://modrinth.com/news/article/creator-monetization/thumbnail.webp",
"date": "2022-11-12T00:00:00.000Z",
"link": "https://modrinth.com/news/article/creator-monetization"
},
{
"title": "Modrinth's Carbon Ads experiment",
"summary": "As a step towards implementing author payouts, we're experimenting with a couple different ad providers to see which one works the best for us.",
"summary": "Experimenting with a different ad providers to find one which one works for us.",
"thumbnail": "https://modrinth.com/news/article/carbon-ads/thumbnail.webp",
"date": "2022-09-08T00:00:00.000Z",
"link": "https://modrinth.com/news/article/carbon-ads"
@@ -149,14 +149,14 @@
},
{
"title": "This week in Modrinth development: Filters and Fixes",
"summary": "After a great first week since Modrinth launched out of beta, we have continued to improve the user interface based on feedback.",
"summary": "Continuing to improve the user interface after a great first week since Modrinth launched out of beta.",
"thumbnail": "https://modrinth.com/news/article/knossos-v2.1.0/thumbnail.webp",
"date": "2022-03-09T00:00:00.000Z",
"link": "https://modrinth.com/news/article/knossos-v2.1.0"
},
{
"title": "Now showing on Modrinth: A new look!",
"summary": "After months of relatively quiet development, Modrinth has released many new features and improvements, including a redesign. Read on to learn more!",
"summary": "Releasing many new features and improvements, including a redesign!",
"thumbnail": "https://modrinth.com/news/article/redesign/thumbnail.webp",
"date": "2022-02-27T00:00:00.000Z",
"link": "https://modrinth.com/news/article/redesign"

View File

@@ -1,11 +1,8 @@
FROM rust:1.88.0 AS build
ENV PKG_CONFIG_ALLOW_CROSS=1
WORKDIR /usr/src/labrinth
COPY . .
COPY apps/labrinth/.sqlx/ .sqlx/
RUN cargo build --release --package labrinth
RUN SQLX_OFFLINE=true cargo build --release --package labrinth
FROM debian:bookworm-slim
@@ -14,12 +11,9 @@ LABEL org.opencontainers.image.description="Modrinth API"
LABEL org.opencontainers.image.licenses=AGPL-3.0
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates openssl dumb-init \
&& apt-get clean \
&& apt-get install -y --no-install-recommends ca-certificates openssl dumb-init curl \
&& rm -rf /var/lib/apt/lists/*
RUN update-ca-certificates
COPY --from=build /usr/src/labrinth/target/release/labrinth /labrinth/labrinth
COPY --from=build /usr/src/labrinth/apps/labrinth/migrations/* /labrinth/migrations/
COPY --from=build /usr/src/labrinth/apps/labrinth/assets /labrinth/assets

View File

@@ -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)
}
}

View File

@@ -166,10 +166,18 @@ pub async fn test_jre(
path: PathBuf,
major_version: u32,
) -> crate::Result<bool> {
let Ok(jre) = jre::check_java_at_filepath(&path).await else {
return Ok(false);
let jre = match jre::check_java_at_filepath(&path).await {
Ok(jre) => jre,
Err(e) => {
tracing::warn!("Invalid Java at {}: {e}", path.display());
return Ok(false);
}
};
let version = extract_java_version(&jre.version)?;
tracing::info!(
"Expected Java version {major_version}, and found {version} at {}",
path.display()
);
Ok(version == major_version)
}

View File

@@ -231,7 +231,7 @@ pub static DEFAULT_SKINS: LazyLock<Vec<Skin>> = LazyLock::new(|| {
Skin {
texture_key: Arc::from("66206c8f51d13d2d31c54696a58a3e8bcd1e5e7db9888d331d0753129324e4f1"),
name: Some(Arc::from("Party Alex")),
variant: MinecraftSkinVariant::Classic,
variant: MinecraftSkinVariant::Slim,
cape_id: None,
texture: Arc::from(Url::try_from(
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAB3RJTUUH4QoJEjAE4f2wWAAAABd0RVh0U29mdHdhcmUAR0xEUE5HIHZlciAzLjRxhaThAAAACHRwTkdHTEQzAAAAAEqAKR8AAAAEZ0FNQQAAsY8L/GEFAAAABmJLR0QA/wD/AP+gvaeTAAAFFUlEQVR4nO1aXWhcRRS+XeMmClmbNVtTMdjUqlHrX5QSC0oMUnzxoVFBEAqSh4oRBEEEH0ooIvjggyDiSx4EkUgfLC1IEC0+iFKoQluFFNpGqLbotqtNGyVtynW/ac/s2ZN77+z9nb1NPviy586dmdxzZubMzJlxJicnnSBW3x92JX9/+34tH95xpyJPgwwi3VS/bRacFtBR6W5iZ7mk5bzDaIAwSi7W5hXzhJZ6QBAGnn1OsfTw6DIivd0R2wB5x6oBbH+Abax4A3jO8ybyOT8uc7EO4MA01/P0eO6mOz8UaO5ulVCeELasF22jpR7w6J5Oxb5X3nM6N9zlbHjtM/V8PcDTAFAUBKDo3JEfnLF3ruj3kJGGd+fOLDTlzx1o08K5NLvXXZj5QHHp6K/q2ZP1dzpf/VnWQ5sivjmS6badoCM/GoRCFy/+pxUDFz/9SKWBkCmd8nrVw5WFLHeLbWkAalHestwIXHkygJ/yZIB23i4vS7jv4yGXs7+/v4mmCu9+c8QF/Qyy6d3NgTTVzxtn/vPX9W9UA6SyEvzp8c16l8gJ3FguBtIEzEJgsacjkW/N9VL4hvKAc6U2p36jItcGSAQYdxjrNAYnzvTp8f/U93eotMqLWzUpH5WhMc+JMSlpcpTSSVJd0hETLx/8cNn/4P4hlA+4XLukDXLgy9u1XD2yztdwvIzEY4d+Ud2SODezP7ARsJgCaHmMaFJW0EOAO6DKQ39FrrBj09X1/SNf79cOywsv9A5oPn9PtzPSXdChNMAtn438DWGgDcBbNKjlvcA/trtyWhkBfOOWV50HD+zW77jSUsHyvWedoZPfaeI5CxR+3jjicB7a+LJzdHSXImSkndg+pCnzA2tqvbrCC9XGEOLDCUpT75BlgNqx4Oe0UNiy+xuHSFj87YQigeeRJHDlRrefbpJfGr5VvUfvkHm5DPCW73liNgEVg9G0moBCN5cqTRn+na8GVvDPqWtD51RX/c8lZ61Tqrd84z3vBX//OHg1/7W8jXJdOs/5w6VmuRF+SAWx1wFr+4tNBJaOlzzpld/EtOFpgGK9FxRFTwgCd2jrx45rJ7jtrVktU7rMjzQ8g+jyXM4CiawEuUPjM4jfmoLnp54BcMeXmRP0SuzpXa8YBtyZkbMD+JqCp/NewB0fl6WDTAWDg4N6qwsZHH5mTJGe5XtOuZ2V22lJ03ZYEstaLHs5Kc1rKUy/keMBkmHjAXmj9Q+wTWOGFd8DrnsDxI0H2FYgLlOJB3hh4qYZFwxVKAMkHg/4Yt2+ZUpC8Xa9UJVIPIADix0YAazu/ET9QvmFY384W//sMleQNZIeU+Q/tuzp06QYH9Jtj3nJVAwAR0rKk0xGsa1w6gYgI3jRtrKZGSBPtP4Btmn9A2yzMD097XLu++pb9+BsVRGyfC9pdQpLAtjTwxJ8jy/jAV55eHqeacwQdjMU934BnRHi/C+J838TjTHB8fHxNZwZdMpMYTTA1NSUy5nFRxGSOP83YfV+gNzrUxzAL5DJnzGGZHla9tLS16scl3lQk8Y/fuV9AHl5K1UfwHeDct9vigPww1Eu83JBdeB+QJaXLn2HAI/hAzxe0MplpijA3YDbxnamUrcffA3AWw/wa0F5dO51vB5E2+iYeGBbY2pbqjN6MCiXiD0LJHW/wBYSnQah0JO79jaxHZQMwopfB6RigLD3C2xitQekEQ+Icr/AGuLGA+LeL5Bn/Pz8X94JiHL+b+L/USxRwWwDOUgAAAAASUVORK5CYII="

View File

@@ -12,8 +12,8 @@ pub mod pack;
pub mod process;
pub mod profile;
pub mod settings;
pub mod update; // [AR] Feature
pub mod tags;
pub mod download; // AstralRinth
pub mod worlds;
pub mod data {

View 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(())
}
}

View File

@@ -151,6 +151,41 @@ pub enum ErrorKind {
"A skin texture must have a dimension of either 64x64 or 64x32 pixels"
)]
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)]

View File

@@ -14,7 +14,7 @@ use chrono::Utc;
use daedalus as d;
use daedalus::minecraft::{LoggingSide, RuleAction, VersionInfo};
use daedalus::modded::LoaderVersion;
use rand::seq::SliceRandom; // AstralRinth
use rand::seq::SliceRandom; // [AR] Feature
use regex::Regex;
use serde::Deserialize;
use st::Profile;
@@ -633,6 +633,20 @@ pub async fn launch_minecraft(
command.arg("--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED");
}
// FIXME: Fix ElyBy integration with this patch.
// [AR] Patch
if credentials.access_token == "null" && credentials.refresh_token == "null" {
if version_jar == "1.16.4" || version_jar == "1.16.5" {
let invalid_url = "https://invalid.invalid";
tracing::info!("✅ JVM args is patched by AstralRinth for MC {}", 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));
}
}
command
.arg("com.modrinth.theseus.MinecraftLaunch")
.arg(version_info.main_class.clone())
@@ -730,6 +744,7 @@ pub async fn launch_minecraft(
}
}
// [AR] Feature
let selected_phrase = ACTIVE_STATE.choose(&mut rand::thread_rng()).unwrap();
let _ = state
.discord_rpc

View File

@@ -8,7 +8,7 @@ and launching Modrinth mod packs
#![deny(unused_must_use)]
#[macro_use]
mod util;
pub mod util; // [AR] Refactor
mod api;
mod config;

View File

@@ -1,17 +1,32 @@
use crate::ErrorKind;
use crate::state::DirectoryInfo;
use sqlx::sqlite::{
SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions,
};
use sqlx::{Pool, Sqlite};
use tokio::time::Instant;
use std::collections::HashMap;
use std::str::FromStr;
use std::time::Duration;
use tokio::time::Instant;
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(
crate::ErrorKind::FSError(
"Could not find valid config dir".to_string(),
),
ErrorKind::FSError("Could not find valid config dir".to_string()),
)?;
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_exists = db_path.exists();
let uri = format!("sqlite:{}", db_path.display());
let conn_options = SqliteConnectOptions::from_str(&uri)?
@@ -33,22 +47,6 @@ pub(crate) async fn connect() -> crate::Result<Pool<Sqlite>> {
.connect_with(conn_options)
.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)
}
@@ -74,62 +72,103 @@ async fn stale_data_cleanup(pool: &Pool<Sqlite>) -> crate::Result<()> {
Ok(())
}
/*
// Patch by AstralRinth - 08.07.2025
Problem files:
// [AR] Patch fix
Problem files, view detailed information in .gitattributes:
/packages/app-lib/migrations/20240711194701_init.sql !eol
CRLF -> 4c47e326f16f2b1efca548076ce638d4c90dd610172fe48c47d6de9bc46ef1c5abeadfdea05041ddd72c3819fa10c040
LF -> e973512979feac07e415405291eefafc1ef0bd89454958ad66f5452c381db8679c20ffadab55194ecf6ba8ec4ca2db21
/packages/app-lib/migrations/20240813205023_drop-active-unique.sql !eol
CRLF -> C8FD2EFE72E66E394732599EA8D93CE1ED337F098697B3ADAD40DD37CC6367893E199A8D7113B44A3D0FFB537692F91D
LF -> 5b53534a7ffd74eebede234222be47e1d37bd0cc5fee4475212491b0c0379c16e3079e08eee0af959b1fa20835eeb206
/packages/app-lib/migrations/20240930001852_disable-personalized-ads.sql !eol
CRLF -> C0DE804F171B5530010EDAE087A6E75645C0E90177E28365F935C9FDD9A5C68E24850B8C1498E386A379D525D520BC57
LF -> c0de804f171b5530010edae087a6e75645c0e90177e28365f935c9fdd9a5c68e24850b8c1498e386a379d525d520bc57
/packages/app-lib/migrations/20241222013857_feature-flags.sql !eol
CRLF -> 6B6F097E5BB45A397C96C3F1DC9C2A18433564E81DB264FE08A4775198CCEAC03C9E63C3605994ECB19C281C37D8F6AE
LF -> c17542cb989a0466153e695bfa4717f8970feee185ca186a2caa1f2f6c5d4adb990ab97c26cacfbbe09c39ac81551704
*/
async fn fix_modrinth_issued_migrations(
pool: &Pool<Sqlite>,
) -> crate::Result<()> {
pub(crate) async fn apply_migration_fix(eol: &str) -> crate::Result<bool> {
let started = Instant::now();
tracing::info!("Fixing modrinth issued migrations");
sqlx::query(
r#"
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();
// Create connection to the database without migrations
let pool = connect_without_migrate().await?;
tracing::info!(
"✅ Fixed all known modrinth-issued migrations in {:.2?}",
elapsed
"⚙️ Patching Modrinth corrupted migration checksums using EOL standard: {eol}"
);
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)
}

View File

@@ -22,6 +22,7 @@ pub struct DirectoryInfo {
impl DirectoryInfo {
// Get the settings directory
// init() is not needed for this function
// [AR] Patch fix. From PR.
pub fn get_initial_settings_dir() -> Option<PathBuf> {
Self::env_path("THESEUS_CONFIG_DIR").or_else(|| {
if std::env::current_dir().ok()?.join("portable.txt").exists() {

View File

@@ -1,16 +1,17 @@
// [AR] Feature
use std::{
sync::{atomic::AtomicBool, Arc},
time::{SystemTime, UNIX_EPOCH}, // AstralRinth
time::{SystemTime, UNIX_EPOCH},
};
use discord_rich_presence::{
activity::{Activity, Assets, Timestamps}, // AstralRinth
activity::{Activity, Assets, Timestamps}, // [AR] Feature
DiscordIpc, DiscordIpcClient,
};
use rand::seq::SliceRandom; // AstralRinth
use rand::seq::SliceRandom; // [AR] Feature
use tokio::sync::RwLock;
use crate::util::utils; // AstralRinth
use crate::util::utils; // [AR] Feature
use crate::State;
pub struct DiscordGuard {

View File

@@ -213,7 +213,7 @@ pub async fn login_finish(
Ok(credentials)
}
// Patched by AstralRinth
// [AR] Feature
#[tracing::instrument]
pub async fn offline_auth(
name: &str,
@@ -790,7 +790,7 @@ const MICROSOFT_CLIENT_ID: &str = "00000000402b5328";
const REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf";
const REQUESTED_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
/* AstralRinth
/* [AR] Fix
* Weird visibility issue that didn't reproduce before
* Had to make DeviceToken and RequestWithDate pub(crate) to fix compilation error
*/

View File

@@ -3,5 +3,5 @@ pub mod fetch;
pub mod io;
pub mod jre;
pub mod platform;
pub mod utils; // AstralRinth
pub mod utils; // [AR] Feature
pub mod server_ping;

View File

@@ -1,21 +1,366 @@
use crate::api::update;
use crate::state::db;
///
/// [AR] Feature Utils
///
use crate::{Result, State};
use serde::{Deserialize, Serialize};
use tokio::io;
use std::process;
use tokio::{fs, io};
/*
AstralRinth Utils
*/
const PACKAGE_JSON_CONTENT: &str =
// include_str!("../../../../apps/app-frontend/package.json");
include_str!("../../../../apps/app/tauri.conf.json");
#[derive(Serialize, Deserialize)]
pub struct Launcher {
pub version: String
pub version: String,
}
pub fn read_package_json() -> io::Result<Launcher> {
// Deserialize the content of package.json into a Launcher struct
let launcher: Launcher = serde_json::from_str(PACKAGE_JSON_CONTENT)?;
#[derive(Debug, Deserialize)]
struct Artifact {
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)
}
/// ### 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)
/// 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");
tracing::info!(
"[AR] • Attempting to download AuthLib {}.",
authlib_version
);
download_authlib(
&minecraft_library_metadata,
authlib_version,
minecraft_version,
is_mojang,
)
.await
}
/// ### AR • AuthLib (Ely By)
/// Downloads the AuthLib file from Mojang libraries or Git Astralium services.
async fn download_authlib(
minecraft_library_metadata: &Library,
authlib_version: &str,
minecraft_version: &str,
is_mojang: bool,
) -> Result<bool> {
let state = State::get().await?;
let (url, path) = extract_download_info(minecraft_library_metadata, minecraft_version)?;
let mut download_url = url.to_string();
let full_path = state.directories.libraries_dir().join(path);
if !is_mojang {
tracing::info!(
"[AR] • Attempting to download AuthLib from Git Astralium"
);
download_url = extract_ely_authlib_url(authlib_version).await?;
}
tracing::info!("[AR] • Downloading AuthLib from URL: {}", download_url);
let bytes = fetch_bytes_from_url(&download_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_ely_authlib_url(authlib_version: &str) -> Result<String> {
let url = "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_version))
.unwrap_or(false)
})
.ok_or_else(|| {
crate::ErrorKind::ParseError {
reason: format!(
"No matching asset for authlib-{}.jar",
authlib_version
),
}
.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()
})?;
Ok(download_url.to_string())
}
/// ### 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_download_info<'a>(
minecraft_library_metadata: &'a Library,
minecraft_version: &str,
) -> Result<(&'a str, &'a str)> {
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.as_deref().ok_or_else(|| {
crate::ErrorKind::MinecraftMetadataNotFound {
minecraft_version: minecraft_version.to_string(),
}
.as_error()
})?;
let path = artifact.path.as_deref().ok_or_else(|| {
crate::ErrorKind::MinecraftMetadataNotFound {
minecraft_version: minecraft_version.to_string(),
}
.as_error()
})?;
Ok((url, path))
}
/// ### AR • AuthLib (Ely By)
/// 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 authlib: HTTP {}", status);
return Err(crate::ErrorKind::NetworkErrorOccurred {
error: format!("Failed to download authlib: 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())
}
/// ### AR • Migration
/// 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 • 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(())
}

View File

@@ -83,21 +83,21 @@ export const TwitterIcon = _TwitterIcon
export const WindowsIcon = _WindowsIcon
export const YouTubeIcon = _YouTubeIcon
// AstralRinth Icons
// [AR] Feature. Icons
import _PirateIcon from './icons/pirate.svg?component'
import _MicrosoftIcon from './icons/microsoft.svg?component'
import _PirateShipIcon from './icons/pirate-ship.svg?component'
import _AstralRinthLogo from './icons/astralrinth-logo.svg?component'
// AstralRinth Exports
// [AR] Feature. Exports
export const PirateIcon = _PirateIcon
export const MicrosoftIcon = _MicrosoftIcon
export const PirateShipIcon = _PirateShipIcon
export const AstralRinthLogo = _AstralRinthLogo
export { default as CapeModel } from './models/cape.gltf?url'
// Skin Models
export { default as ClassicPlayerModel } from './models/classic-player.gltf?url'
export { default as SlimPlayerModel } from './models/slim-player.gltf?url'

View File

@@ -1,92 +0,0 @@
{
"asset": { "version": "2.0", "generator": "Blockbench 4.12.4 glTF exporter" },
"scenes": [{ "nodes": [1], "name": "blockbench_export" }],
"scene": 0,
"nodes": [
{
"rotation": [0, 0, 0.19509032201612825, 0.9807852804032304],
"translation": [0.15625, 1, 0],
"name": "Cape",
"mesh": 0
},
{ "children": [0] }
],
"bufferViews": [
{ "buffer": 0, "byteOffset": 0, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 288, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 576, "byteLength": 192, "target": 34962, "byteStride": 8 },
{ "buffer": 0, "byteOffset": 768, "byteLength": 72, "target": 34963 }
],
"buffers": [
{
"byteLength": 840,
"uri": "data:application/octet-stream;base64,AAAAPQAAAAAAAKA+AAAAPQAAAAAAAKC+AAAAPQAAgL8AAKA+AAAAPQAAgL8AAKC+AAAAvQAAAAAAAKC+AAAAvQAAAAAAAKA+AAAAvQAAgL8AAKC+AAAAvQAAgL8AAKA+AAAAvQAAAAAAAKC+AAAAPQAAAAAAAKC+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAvQAAgL8AAKA+AAAAPQAAgL8AAKA+AAAAvQAAgL8AAKC+AAAAPQAAgL8AAKC+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAvQAAgL8AAKA+AAAAPQAAgL8AAKA+AAAAPQAAAAAAAKC+AAAAvQAAAAAAAKC+AAAAPQAAgL8AAKC+AAAAvQAAgL8AAKC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPAAAgD0AADA+AACAPQAAgDwAAAg/AAAwPgAACD8AAEA+AACAPQAAsD4AAIA9AABAPgAACD8AALA+AAAIPwAAgDwAAAA9AACAPAAAgD0AADA+AAAAPQAAMD4AAIA9AAAwPgAAAD0AAKg+AAAAPQAAMD4AAAAAAACoPgAAAAAAAEA+AACAPQAAMD4AAIA9AABAPgAACD8AADA+AAAIPwAAAAAAAIA9AACAPAAAgD0AAAAAAAAIPwAAgDwAAAg/AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUA"
}
],
"accessors": [
{
"bufferView": 0,
"componentType": 5126,
"count": 24,
"max": [0.03125, 0, 0.3125],
"min": [-0.03125, -1, -0.3125],
"type": "VEC3"
},
{
"bufferView": 1,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 2,
"componentType": 5126,
"count": 24,
"max": [0.34375, 0.53125],
"min": [0, 0],
"type": "VEC2"
},
{
"bufferView": 3,
"componentType": 5123,
"count": 36,
"max": [23],
"min": [0],
"type": "SCALAR"
}
],
"materials": [
{
"pbrMetallicRoughness": {
"metallicFactor": 0,
"roughnessFactor": 1,
"baseColorTexture": { "index": 0 }
},
"alphaMode": "MASK",
"alphaCutoff": 0.05,
"doubleSided": true
}
],
"textures": [{ "sampler": 0, "source": 0, "name": "cape.png" }],
"samplers": [{ "magFilter": 9728, "minFilter": 9728, "wrapS": 33071, "wrapT": 33071 }],
"images": [
{
"mimeType": "image/png",
"uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAAAXNSR0IArs4c6QAABCRJREFUaEPtlktoE1EUhm/sIzFpYsBgHyn2oRZt0C5ESxWFYutKNyKIWBBR6kpBd3Ur2J0FRdCiuKmIIG4qLnzgQqTUXSlVqdoXTU0lljRpYtOHI/8dznBnOm1mphMwJWeTzJ0795zznf/cex2MMXam2S/ht38siJ8V1lgd5mOBARerc3rZnmK37rwvCyk2nE6wezMRh+4EncH9B1ql+fkU64rPsaBz5bqh4T7Daxn1Kc5zVNeEePJIci0Aq71bzenY6JChwAnA0OBHQ/OtJLnWNwqAy61R1tO3k891ueRKoDKwtqbv7MGbgCnfRgHQoqG9hyX4hc/yiho+/HNqVPFtdj2jwTpQgd/RKT7/0ZUku/o4qAJw50KYXbzr4e+3BioYzc3kwGzAUCLWFw2+UBiCb3bNTDHiPQeAP3CGNmg/6VcSBpDu3hhvDQpOCwABwrQKsRIsqYAChy/EQAXwlPiZ3a3igNPkXIyTjr8o5L7PPdzGf59c+sV/faeWeIIIAHPJ8M3kc7l1K09LKghWAIgqoPZ7djPFTlxb4D6yAgBOQfntrUVWVeRk44tplXJorOVGkVIJTBCTpw9ECFYBIEkyMfmsAXh3u1qi5MlxbbGX/x1ZSCjBAAwgfPr6h49R5UVavk0FXC2wju5p07s6iqEF0PtqSlEf+bKzDRwdgaDU7AkoySJ5Oo/D6ZRq/H0yyuJ/lxkSheG/FgCNm7kL0BoEAG32sqtY1YIHd2/mGzTMVgD3y2slqjgW9xY6ma9AThAARIMiBtOpjADwTWc0bEkB+FZsSTxTW0KBsGPXx0yvrUpEeHAAQIM7wBJLcu8DwHaP/H8i6VSND6SiSjBlRW4WWUypVIBbIgzjVgB0tpdKqLReS0KVPTMTfH0ra2cEgAmAAEd+l1z52LybqwBQYAQAyZPh6gtDW4jjuC4fHx8wXSm0JDbeI95SRYHiFZlUaWVtPQiKAugl5E8ASAUYiy8vc0C474uGasPE5PHc4g0wK/f4obom6UNimol7kTZwQLANwOuqBokqDEeQf4lfvvnNxZJcBTBAGZplWQcQ3tcgwY9oWlXinRW4ugoAgNAWWe6ocn1QvgyRTUb4RZFVljlY/3hSBYCqb6cCZo8ekuATVRZPI/gQW8FWAI1VHganVgDQUajdA6y2gAgAcZFBjTBSpG0AcAqc3VVmCMDTbxGWZvIRCaMNkJ7pFMCzVQD4liCQ8kRFUlvaBkCvL+wYw2ZmNUgCgLajFhRPJlv3ADuS1VtjPQCk823S574ffN/x1dQqy8dHR5SN2Spcbaymz2mjwNYDAD4IQn3TDpVLQIAqNjwAANRrAdoI/3sAOF7Xe1khCNQGVH1bL0JGJb1R52VtD8gVYHkAuVKpbMWZV0C2yObKunkF5EqlshVnXgHZIpsr6/4DlbxcPydnT74AAAAASUVORK5CYII="
}
],
"meshes": [
{
"primitives": [
{
"mode": 4,
"attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 },
"indices": 3,
"material": 0
}
]
}
]
}

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,39 @@
// [AR] Feature
.neon-button.neon :deep(:is(button, a, .button-like)),
.neon-button.neon :slotted(:is(button, a, .button-like)),
.neon-button.neon :slotted(*) :is(button, a, .button-like) {
cursor: pointer;
background-color: transparent;
border: 1px solid #3e8cde;
color: #3e8cde;
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.25s ease,
box-shadow 0.3s ease,
transform 0.15s ease;
box-shadow: 0 0 4px rgba(79, 173, 255, 0.5);
}
.bordered {
border-radius: 12px;
}
/* Hover */
.neon-button.neon
:deep(:is(button, a, .button-like):hover):not([disabled]):not(.disabled),
.neon-button.neon
:slotted(:is(button, a, .button-like):hover):not([disabled]):not(.disabled),
.neon-button.neon
:slotted(*) :is(button, a, .button-like):hover:not([disabled]):not(.disabled) {
color: #10fae5;
transform: scale(1.02);
box-shadow:
0 0 4px rgba(16, 250, 229, 0.3),
0 0 8px rgba(16, 250, 229, 0.2);
text-shadow:
0 0 2px rgba(16, 250, 229, 0.4),
0 0 4px rgba(16, 250, 229, 0.25);
}

View File

@@ -0,0 +1,37 @@
// [AR] Feature
.neon-icon {
background-color: transparent;
color: #3e8cde;
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: transform 0.25s ease, color 0.25s ease, text-shadow 0.25s ease;
cursor: pointer;
display: inline-block;
}
/* Hover */
.neon-icon:hover {
color: #10fae5;
transform: scale(1.05);
text-shadow:
0 0 2px rgba(16, 250, 229, 0.4),
0 0 4px rgba(16, 250, 229, 0.25);
}
.neon-icon.pulse {
position: relative;
animation: neon-pulse 1s ease-in-out infinite;
filter: drop-shadow(0 0 6px #10fae5);
box-shadow: none;
}
@keyframes neon-pulse {
0%, 100% {
filter: drop-shadow(0 0 4px #10fae5);
}
50% {
filter: drop-shadow(0 0 12px #10fae5);
}
}

View File

@@ -0,0 +1,28 @@
// [AR] Feature
.neon-text {
background-color: transparent;
color: #3e8cde;
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.25s ease,
box-shadow 0.3s ease,
transform 0.15s ease;
white-space: normal;
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 100%;
display: inline-block;
padding: 4px 8px;
}
/* Hover */
.neon-text:hover:not([disabled]):not(.disabled) {
color: #10fae5;
text-shadow:
0 0 2px rgba(16, 250, 229, 0.4),
0 0 4px rgba(16, 250, 229, 0.25);
}

View File

@@ -2,6 +2,7 @@
title: A New Chapter for Modrinth Servers
summary: Modrinth Servers is now fully operated in-house by the Modrinth Team.
date: 2025-03-13T00:00:00+00:00
authors: ['MpxzqsyW', 'Dc7EYhxG']
---
Over the few months, Modrinth has seen incredible interest towards our Servers product, and with significant growth, our vision for what Modrinth Servers can be has evolved alongside it. To continue striving towards our goal of providing the best place to get your own Minecraft multiplayer server, weve made the decision to bring our server hosting fully in-house.

View File

@@ -2,6 +2,7 @@
title: Accelerating Modrinth's Development
summary: Our fundraiser and the future of Modrinth!
date: 2023-02-01T12:00:00-08:00
authors: ['MpxzqsyW', 'Dc7EYhxG', '6plzAzU4']
---
**Update: On [April 4, 2024](/news/article/capital-return) we announced that we had returned the remaining $800k in investor capital back to our investors to take a different path. [Read that announcement here](/news/article/capital-return). This article remains here for archival purposes.**

View File

@@ -4,6 +4,7 @@ short_title: Becoming Sustainable
summary: Announcing an update to our monetization program, creator split, and more!
short_summary: Announcing 5x creator revenue and updates to the monetization program.
date: 2024-09-13T12:00:00-08:00
authors: ['MpxzqsyW', 'Dc7EYhxG']
---
Just over 3 weeks ago, we [launched](/news/article/introducing-modrinth-refreshed-site-look-new-advertising-system) our new ads powered by [Aditude](https://www.aditude.com/). These ads have allowed us to improve creator revenue drastically and become sustainable. Read on for more info!

View File

@@ -2,6 +2,7 @@
title: A Sustainable Path Forward for Modrinth
summary: Our capital return and whats next.
date: 2024-04-04T12:00:00-08:00
authors: ['MpxzqsyW']
---
Over three years ago, I started Modrinth: a new Minecraft modding platform built on community principles, a fully open-source codebase, and a focus on creators.

View File

@@ -1,7 +1,8 @@
---
title: Modrinth's Carbon Ads experiment
summary: "As a step towards implementing author payouts, we're experimenting with a couple different ad providers to see which one works the best for us."
summary: 'Experimenting with a different ad providers to find one which one works for us.'
date: 2022-09-08
authors: ['6plzAzU4']
---
**Update 10/24:** A month and a half ago we began testing Carbon Ads on Modrinth, and in the end, using Carbon did not work out. After disabling ads with tracking in them, the revenue was about equal to or worse than what we were generating previously with EthicalAds. Effective today, we are switching our ads provider back to EthicalAds for the time being.

View File

@@ -1,7 +1,8 @@
---
title: Creators can now make money on Modrinth!
summary: "Yes, you read the title correctly: Modrinth's creator monetization program, also known as payouts, is now in an open beta phase. Read on for more information!"
summary: 'Introducing the Creator Monetization Program allowing creators to earn revenue from their projects.'
date: 2022-11-12
authors: ['6plzAzU4']
---
Yes, you read the title correctly: Modrinth's Creator Monetization Program, also known as payouts, is now in an open beta phase. All of the money that project owners have earned since August 1st is available to claim **right now**!

View File

@@ -4,6 +4,7 @@ short_title: The Creator Update
summary: December may be over, but were not done giving gifts.
short_summary: Adding analytics, orgs, collections, and more!
date: 2024-01-06T12:00:00-08:00
authors: ['6plzAzU4']
---
December may be over, but that doesnt mean were done giving gifts here at Modrinth. Over the past few months, weve been cooking up a whole bunch of new features for everyone to enjoy. Now seems like as good of a time as ever to bring you our Creator Update! Buckle up, because this is a big one.

View File

@@ -2,6 +2,7 @@
title: Creator Updates, July 2025
summary: Addressing recent growth and growing pains that have been affecting creators.
date: 2025-07-01T21:20:00-07:00
authors: ['MpxzqsyW']
---
Hey all,

View File

@@ -4,6 +4,7 @@ short_title: Modrinth+ and New Ads
summary: Learn about this major update to Modrinth.
short_summary: Introducing a new ad system, a subscription to remove ads, and a redesign of the website!
date: 2024-08-21T12:00:00-08:00
authors: ['MpxzqsyW', 'Dc7EYhxG']
---
Weve got a big launch with tons of new stuff today and some important updates about Modrinth. Read on, because we have a lot to cover!

View File

@@ -3,6 +3,7 @@ title: Correcting Inflated Download Counts due to Rate Limiting Issue
short_title: Correcting Inflated Download Counts
summary: A rate limiting issue caused inflated download counts in certain countries.
date: 2023-11-10T12:00:00-08:00
authors: ['6plzAzU4', 'MpxzqsyW']
---
While working on the upcoming analytics update for Modrinth, our team found an issue leading to higher download counts from specific countries. This was caused by an oversight with regards to rate limiting, or in other words, certain files being downloaded over and over again. **Importantly, this did not affect creator payouts**; only our analytics. Approximately 15.4% of Modrinth downloads were found to be over-counted. These duplicates have been identified and are being removed from project download counts and analytics. Read on to learn about the cause of this error and how we fixed it.

View File

@@ -1,7 +1,8 @@
---
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.'
date: 2022-03-09
authors: ['Dc7EYhxG']
---
It's officially been a bit over a week since Modrinth launched out of beta. We have continued to make improvements to the user experience on [the website](https://modrinth.com).

View File

@@ -2,6 +2,7 @@
title: Beginner's Guide to Licensing your Mods
summary: Software licenses; the nitty-gritty legal aspect of software development. They're more important than you think.
date: 2021-05-16
authors: ['6plzAzU4', 'aNd6VJql']
---
Why do you need to license your software? What are those licenses for anyway? These questions are more important than you think

View File

@@ -2,6 +2,7 @@
title: 'Changes to Modrinth Modpacks'
summary: 'CurseForge CDN links requested to be removed by the end of the month'
date: 2022-05-28
authors: ['MpxzqsyW', 'Dc7EYhxG']
---
CurseForge CDN links requested to be removed by the end of the month

View File

@@ -2,6 +2,7 @@
title: 'Modrinth Modpacks: Now in alpha testing'
summary: After over a year of development, we're happy to announce that modpack support is now in alpha testing.
date: 2022-05-15
authors: ['6plzAzU4']
---
After over a year of development, Modrinth is happy to announce that modpack support is now in alpha testing!

View File

@@ -4,6 +4,7 @@ short_title: Modrinth App Beta and Upgraded Authentication
summary: Changing the modded Minecraft landscape with the new Modrinth App, alongside several other major features.
short_summary: Launching Modrinth App Beta and upgrading authentication.
date: 2023-08-05T12:00:00-08:00
authors: ['6plzAzU4']
---
The past few months have been a bit quiet on our part, but that doesnt mean we havent been working on anything. In fact, this is quite possibly our biggest update yet, bringing the much-anticipated Modrinth App to general availability, alongside several other major features. Lets get right into it!

View File

@@ -2,6 +2,7 @@
title: Welcome to Modrinth Beta
summary: 'After six months of work, Modrinth enters Beta, helping modders host their mods with ease!'
date: 2020-12-01
authors: ['Dc7EYhxG']
---
After six months of work, Modrinth enters Beta, helping modders host their mods with ease!

View File

@@ -4,6 +4,7 @@ short_title: Introducing Modrinth Servers
summary: Fast, simple, reliable servers directly integrated into Modrinth.
short_summary: Host your next Minecraft server with Modrinth.
date: 2024-11-02T22:00:00-08:00
authors: ['MpxzqsyW', 'Dc7EYhxG']
---
It's been almost _four_ years since we publicly launched Modrinth Beta. Today, we're thrilled to unveil a new beta release of a product we've been eagerly developing: Modrinth Servers.

View File

@@ -2,6 +2,7 @@
title: Plugins and Resource Packs now have a home on Modrinth
summary: 'A small update with a big impact: plugins and resource packs are now available on Modrinth!'
date: 2022-08-27
authors: ['6plzAzU4']
---
With the addition of modpacks, creating new project types has become a lot easier. Our first additions to our new system are plugins and resource packs. We'll also be working on adding datapacks, shader packs, and worlds after payouts are released.

View File

@@ -1,9 +1,10 @@
---
title: 'A Pride Month Success: Over $8,400 Raised for The Trevor Project!'
short_title: Pride Month Fundraiser 2025
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.
short_summary: A reflection on our Pride Month fundraiser campaign.
date: 2025-07-01T14:00:00-04:00
authors: ['6plzAzU4', 'bOHH0P9Z', '2cqK8Q5p', 'vNcGR3Fd']
---
What an incredible Pride Month! This June, we came together to support [The Trevor Project](https://www.thetrevorproject.org/), an essential organization providing crisis support and life-saving resources for LGBTQ+ young people. We are absolutely thrilled to announce that our community raised a stellar **$2,395**. That's not all, though — during the campaign, some donations were matched by H&M and the Trevor Project's Board of Directors up to **six times**, meaning the final impact of Modrinth's donations is a whopping **$8,464**. Our team was also in the top ten for most funds raised this year!

View File

@@ -1,7 +1,8 @@
---
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!'
date: 2022-02-27
authors: ['6plzAzU4']
---
After months of relatively quiet development, Modrinth has released many new features and improvements, including a redesign. While we've been a bit silent recently on the website and blog, our [Discord server][Discord] has activity on the daily. Join us there and follow along with the development channels for the very latest information!

View File

@@ -2,6 +2,7 @@
title: 'Skins — Now in Modrinth App!'
summary: 'Customize your look, save your favorite skins, and swap them out in a flash, all within Modrinth App.'
date: 2025-07-06T16:45:00-07:00
authors: [bOHH0P9Z, Dc7EYhxG]
---
We're thrilled to roll out Modrinth App **v0.10** with a beta release of one of our most highly requested features, the **Skins page**. The Skins page allows you to manage all of your Minecraft skins directly within Modrinth App. You can see all your saved custom skins and the default Minecraft skins in one convenient place.

View File

@@ -2,6 +2,7 @@
title: 'Two years of Modrinth: a retrospective'
summary: The history of Modrinth as we know it from December 2020 to December 2022.
date: 2023-01-07
authors: ['6plzAzU4']
---
Let's rewind a bit and take a look at the past two years of Modrinth's history. We've come so far from our pre-beta HexFabric days to today. A good portion of our pre-beta history can be found in the [What is Modrinth](../what-is-modrinth) blog post, but Modrinth obviously is not the same platform it was two years ago.

View File

@@ -2,6 +2,7 @@
title: Modrinth's Anniversary Update
summary: Marking two years of Modrinth and discussing our New Year's Resolutions for 2023.
date: 2023-01-07
authors: ['6plzAzU4']
---
Modrinth initially [went into beta](../modrinth-beta) on November 30th, 2020. Just over a month ago was November 30th, 2022, marking **two years** since Modrinth was generally available as a platform for everyone to use. Today, we're proud to announce the Anniversary Update, celebrating both two years of Modrinth as well as the coming of the new year, and we'll be discussing our New Year's Resolutions for 2023.

View File

@@ -2,6 +2,7 @@
title: What is Modrinth?
summary: "Hello, we are Modrinth an open source mods hosting platform. Sounds dry, doesn't it? So let me tell you our story and I promise, it won't be boring!"
date: 2020-11-27
authors: ['aNd6VJql']
---
Hello, we are Modrinth an open source mods hosting platform. Sounds dry, doesn't it? So let me tell you our story and I promise, it won't be boring!

View File

@@ -2,6 +2,7 @@
title: 'Malware Discovery Disclosure: "Windows Borderless" mod'
summary: Threat Analysis and Plan of Action
date: 2024-05-07T12:00:00-08:00
authors: ['Dc7EYhxG', 'MpxzqsyW']
---
This is a disclosure of a malicious mod discovered to be hosted on the Modrinth platform. It is important to not panic or jump to conclusions, please carefully read the [Am I Affected?](#am-i-affected) and [Threat Summary](#threat-summary) sections.

View File

@@ -59,7 +59,7 @@ async function compileArticles() {
const src = await fs.readFile(file, 'utf8')
const { content, data } = matter(src)
const { title, summary, date, slug: frontSlug, ...rest } = data
const { title, summary, date, slug: frontSlug, authors: authorsData, ...rest } = data
if (!title || !summary || !date) {
console.error(`❌ Missing required frontmatter in ${file}. Required: title, summary, date`)
process.exit(1)
@@ -71,6 +71,8 @@ async function compileArticles() {
removeComments: true,
})
const authors = authorsData ? authorsData : []
const slug = frontSlug || path.basename(file, '.md')
const varName = toVarName(slug)
const exportFile = path.join(COMPILED_DIR, `${varName}.ts`)
@@ -91,6 +93,7 @@ export const article = {
summary: ${JSON.stringify(summary)},
date: ${JSON.stringify(date)},
slug: ${JSON.stringify(slug)},
authors: ${JSON.stringify(authors)},
thumbnail: ${thumbnailPresent},
${Object.keys(rest)
.map((k) => `${k}: ${JSON.stringify(rest[k])},`)

View File

@@ -5,5 +5,6 @@ export const article = {
summary: 'Modrinth Servers is now fully operated in-house by the Modrinth Team.',
date: '2025-03-13T00:00:00.000Z',
slug: 'a-new-chapter-for-modrinth-servers',
authors: ['MpxzqsyW', 'Dc7EYhxG'],
thumbnail: true,
}

View File

@@ -5,5 +5,6 @@ export const article = {
summary: 'Our fundraiser and the future of Modrinth!',
date: '2023-02-01T20:00:00.000Z',
slug: 'accelerating-development',
authors: ['MpxzqsyW', 'Dc7EYhxG', '6plzAzU4'],
thumbnail: false,
}

View File

@@ -5,6 +5,7 @@ export const article = {
summary: 'Announcing an update to our monetization program, creator split, and more!',
date: '2024-09-13T20:00:00.000Z',
slug: 'becoming-sustainable',
authors: ['MpxzqsyW', 'Dc7EYhxG'],
thumbnail: true,
short_title: 'Becoming Sustainable',
short_summary: 'Announcing 5x creator revenue and updates to the monetization program.',

View File

@@ -5,5 +5,6 @@ export const article = {
summary: 'Our capital return and whats next.',
date: '2024-04-04T20:00:00.000Z',
slug: 'capital-return',
authors: ['MpxzqsyW'],
thumbnail: false,
}

View File

@@ -2,9 +2,9 @@
export const article = {
html: () => import(`./carbon_ads.content`).then((m) => m.html),
title: "Modrinth's Carbon Ads experiment",
summary:
"As a step towards implementing author payouts, we're experimenting with a couple different ad providers to see which one works the best for us.",
summary: 'Experimenting with a different ad providers to find one which one works for us.',
date: '2022-09-08T00:00:00.000Z',
slug: 'carbon-ads',
authors: ['6plzAzU4'],
thumbnail: true,
}

View File

@@ -3,8 +3,9 @@ export const article = {
html: () => import(`./creator_monetization.content`).then((m) => m.html),
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!",
'Introducing the Creator Monetization Program allowing creators to earn revenue from their projects.',
date: '2022-11-12T00:00:00.000Z',
slug: 'creator-monetization',
authors: ['6plzAzU4'],
thumbnail: true,
}

View File

@@ -5,6 +5,7 @@ export const article = {
summary: 'December may be over, but were not done giving gifts.',
date: '2024-01-06T20:00:00.000Z',
slug: 'creator-update',
authors: ['6plzAzU4'],
thumbnail: true,
short_title: 'The Creator Update',
short_summary: 'Adding analytics, orgs, collections, and more!',

View File

@@ -5,5 +5,6 @@ export const article = {
summary: 'Addressing recent growth and growing pains that have been affecting creators.',
date: '2025-07-02T04:20:00.000Z',
slug: 'creator-updates-july-2025',
authors: ['MpxzqsyW'],
thumbnail: false,
}

View File

@@ -5,6 +5,7 @@ export const article = {
summary: 'Learn about this major update to Modrinth.',
date: '2024-08-21T20:00:00.000Z',
slug: 'design-refresh',
authors: ['MpxzqsyW', 'Dc7EYhxG'],
thumbnail: true,
short_title: 'Modrinth+ and New Ads',
short_summary:

View File

@@ -5,6 +5,7 @@ export const article = {
summary: 'A rate limiting issue caused inflated download counts in certain countries.',
date: '2023-11-10T20:00:00.000Z',
slug: 'download-adjustment',
authors: ['6plzAzU4', 'MpxzqsyW'],
thumbnail: false,
short_title: 'Correcting Inflated Download Counts',
}

View File

@@ -3,8 +3,9 @@ export const article = {
html: () => import(`./knossos_v2_1_0.content`).then((m) => m.html),
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.',
'Continuing to improve the user interface after a great first week since Modrinth launched out of beta.',
date: '2022-03-09T00:00:00.000Z',
slug: 'knossos-v2.1.0',
authors: ['Dc7EYhxG'],
thumbnail: true,
}

View File

@@ -6,5 +6,6 @@ export const article = {
"Software licenses; the nitty-gritty legal aspect of software development. They're more important than you think.",
date: '2021-05-16T00:00:00.000Z',
slug: 'licensing-guide',
authors: ['6plzAzU4', 'aNd6VJql'],
thumbnail: true,
}

View File

@@ -5,5 +5,6 @@ export const article = {
summary: 'CurseForge CDN links requested to be removed by the end of the month',
date: '2022-05-28T00:00:00.000Z',
slug: 'modpack-changes',
authors: ['MpxzqsyW', 'Dc7EYhxG'],
thumbnail: true,
}

View File

@@ -6,5 +6,6 @@ export const article = {
"After over a year of development, we're happy to announce that modpack support is now in alpha testing.",
date: '2022-05-15T00:00:00.000Z',
slug: 'modpacks-alpha',
authors: ['6plzAzU4'],
thumbnail: true,
}

View File

@@ -6,6 +6,7 @@ export const article = {
'Changing the modded Minecraft landscape with the new Modrinth App, alongside several other major features.',
date: '2023-08-05T20:00:00.000Z',
slug: 'modrinth-app-beta',
authors: ['6plzAzU4'],
thumbnail: false,
short_title: 'Modrinth App Beta and Upgraded Authentication',
short_summary: 'Launching Modrinth App Beta and upgrading authentication.',

View File

@@ -6,5 +6,6 @@ export const article = {
'After six months of work, Modrinth enters Beta, helping modders host their mods with ease!',
date: '2020-12-01T00:00:00.000Z',
slug: 'modrinth-beta',
authors: ['Dc7EYhxG'],
thumbnail: true,
}

View File

@@ -5,6 +5,7 @@ export const article = {
summary: 'Fast, simple, reliable servers directly integrated into Modrinth.',
date: '2024-11-03T06:00:00.000Z',
slug: 'modrinth-servers-beta',
authors: ['MpxzqsyW', 'Dc7EYhxG'],
thumbnail: true,
short_title: 'Introducing Modrinth Servers',
short_summary: 'Host your next Minecraft server with Modrinth.',

View File

@@ -5,6 +5,7 @@ export const article = {
summary: "Welcome to the new era of Modrinth. We can't wait to hear your feedback.",
date: '2023-04-01T08:00:00.000Z',
slug: 'new-site-beta',
authors: [],
thumbnail: true,
short_title: '(April Fools 2023) Modrinth Technologies™ beta launch!',
short_summary: 'Power up your experience.',

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