Compare commits

..

46 Commits

Author SHA1 Message Date
Prospector
1b08d2741b Merge remote-tracking branch 'origin/main' into security-txt 2025-07-09 15:50:02 -07: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
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
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
Josiah Glosson
c47bcf665d Fix MinecraftLaunch failing in the case of a package-private main class on Java 8 (#3932)
I don't know of any mod loaders where this is the case, but better be safe than sorry
2025-07-07 15:42:38 +00:00
Prospector
bc90c27e27 Add ?new to url to give it a new key 2025-07-07 01:18:40 -07:00
Prospector
c1be57773a Update changelog 2025-07-07 01:10:51 -07:00
IMB11
315c68912c fix: use watch for links not mount event (#3929) 2025-07-07 08:01:21 +00:00
Prospector
559d203996 Add a hack to temporarily patch Java 8 not working (#3927) 2025-07-07 00:52:41 -07:00
Prospector
54522518c3 Update changelog + blog post time 2025-07-06 16:37:47 -07:00
Prospector
bacb1561d5 Allow http from asset.localhost and textures.minecraft.net on mac (#3922) 2025-07-06 22:31:55 +00:00
IMB11
b8521f926f feat: skins blogpost (#3904)
* feat: skins blogpost

* fix: clarify changelog note

* Update packages/blog/articles/skins-now-in-modrinth-app.md

Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
Signed-off-by: IMB11 <hendersoncal117@gmail.com>

* fix: review issues

* fix: lint

---------

Signed-off-by: IMB11 <hendersoncal117@gmail.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
2025-07-06 21:43:36 +00:00
IMB11
b29672f4b4 fix: model issues & move to @modrinth/assets (#3911)
* fix: model issues & move to `@modrinth/assets`

* revert: vscode settings change

* fix: remove unused props
2025-07-06 21:42:55 +00:00
Alejandro González
a32fe6a41f ci: revamp app build workflow, introduce a new one for release deployment (#3921)
* feat(ci): clean up app release build workflow, set app versions to match tag's

* feat(ci): rename Theseus build workflow, add new release workflow

* chore(ci): minor tweaks to `theseus-build` workflow

* chore: update workflow reference in comments
2025-07-06 21:41:52 +00:00
IMB11
0e35135093 refactor: cleanup & fix caching issues on /app page. (#3919) 2025-07-06 21:41:21 +00:00
Josiah Glosson
31ecace083 Fix launching older Forge versions (#3920) 2025-07-06 19:09:49 +00:00
Alejandro González
e5b134f8f4 feat(app): add free official Java Edition skin packs as default skins (#3913) 2025-07-06 10:16:11 +00:00
Ben
139a4863d1 Fix typo for skin name tag settings (#3903)
Signed-off-by: Ben <67504107+bjsho@users.noreply.github.com>
2025-07-05 19:42:20 +00:00
Prospector
8faea1663a liiiiint 2025-07-05 11:37:41 -07:00
Prospector
ece8a07486 Fix bugs with 0.10.0, update changelog 2025-07-05 11:33:28 -07:00
Alejandro González
0030f35d0c fix(theseus): make SQLx migration checksums match the deployed ones on Windows (#3899) 2025-07-05 03:39:52 +00:00
Prospector
1e24225350 Bump app version to 0.10.1 2025-07-04 20:41:27 -07:00
Prospector
e84a178586 add web changelog 2025-07-04 11:46:31 -07:00
Prospector
0a83ed965e Update changelog time 2025-07-04 11:44:39 -07:00
Erb3
e35981e4aa Merge branch 'main' into security-txt 2025-03-12 20:50:25 +01:00
Erb3
4f3b4a55e2 docs(frontend) careers link in security.txt
Signed-off-by: Erb3 <49862976+Erb3@users.noreply.github.com>
2025-03-12 20:50:15 +01:00
Erb3
51c3797456 Merge branch 'main' into security-txt 2025-01-17 16:33:07 +01:00
Erb3
5f3200ce43 fix(frontend): extend security.txt expiry
It takes so long to merge the PR :(

Signed-off-by: Erb3 <49862976+Erb3@users.noreply.github.com>
2025-01-17 16:32:55 +01:00
Erb3
c4b0f7dcd1 Merge branch 'main' into security-txt 2024-12-07 10:37:07 +01:00
Erb3
c6dee57c40 Merge branch 'main' into security-txt 2024-10-24 17:56:21 +02:00
Erb3
8f29da5739 Merge branch 'main' into security-txt 2024-10-22 21:23:52 +02:00
Erb3
0bc5e954e4 Merge branch 'main' into security-txt 2024-10-20 18:04:58 +02:00
Erb3
7a3ca5fb45 Merge branch 'main' into security-txt 2024-08-26 20:46:33 +02:00
Erb3
28adcdd401 Merge branch 'main' into security-txt 2024-08-25 09:55:12 +02:00
Erb3
414bcaf348 fix(docs): reduce security.txt expiry
This addresses a concern where the security.txt has a long expiration date. Someone could treat this as "use this until then", which we don't want since it's a long time. The specification recommends no longer than one year, as it is to mark as stale.

From the RFC:

> The "Expires" field indicates the date and time after which the data contained in the "security.txt" file is considered stale and should not be used (as per Section 5.3). The value of this field is formatted according to the Internet profiles of [ISO.8601-1] and [ISO.8601-2] as defined in [RFC3339]. It is RECOMMENDED that the value of this field be less than a year into the future to avoid staleness.

Signed-off-by: Erb3 <49862976+Erb3@users.noreply.github.com>
2024-08-22 19:10:51 +02:00
Erb3
727b00855e feat: add security.txt
Security.txt is a well-known (pun intended) file among security researchers, so they don't have to go scavenging for your security information. More information is available on [securitytxt.org](https://securitytxt.org/).

I've set the following values:

- The email to contact with issues, `jai@modrinth.com`. This is the email stated in the security policy. If you wish to not include it here due to spam, you should also not have it as a `mailto` link in the security policy.
- Expiry is set to 2030. By this time Modrinth has become the biggest Minecraft mod distributor, and having expanded into other games. By this time they should also have updated this file.
- English is the preferred language
- The file is located at modrinth.com/.well-known/security.txt
- The security policy is at https://modrinth.com/legal/security

The following values have been left unset:

- PGP key, not sure where this would be located, if there is one
- Acknowledgments. Modrinth does currently not have a site for thanks
- Hiring, as it wants security-related positions
- CSAF, a Common Security Advisory Framework ?
2024-08-22 17:30:32 +02:00
113 changed files with 3069 additions and 517 deletions

1
.dockerignore Symbolic link
View File

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

34
.gitattributes vendored
View File

@@ -1 +1,35 @@
* text=auto eol=lf
# SQLx calculates a checksum of migration scripts at build time to compare
# it with the checksum of the applied migration for the same version at
# runtime, to know if the migration script has been changed, and thus the
# DB schema went out of sync with the code.
#
# However, such checksum treats the script as a raw byte stream, taking
# into account inconsequential differences like different line endings
# in different OSes. When combined with Git's EOL conversion and mixed
# native and cross-compilation scenarios, this leads to existing
# migrations that didn't change having potentially different checksums
# according to the environment they were built in, which can break the
# migration system when deploying the Modrinth App, rendering it
# unusable.
#
# The gitattribute above ensures that all text files are checked out
# with LF line endings, but widely deployed app versions were built
# without this attribute set, which left such line endings variable to
# the platform. Thus, there is no perfect solution to this problem:
# forcing CRLF here would break Linux and macOS users, forcing LF
# breaks Windows users, and leaving it unspecified may still lead to
# line ending differences when cross-compiling from Linux to Windows
# or vice versa, or having Git configured with different line
# conversion settings. Moreover, there is no `eol=native` attribute,
# and using CI-only scripts to convert line endings would make the
# builds differ between CI and most local environments. So, let's pick
# the least bad option: let Git handle line endings using its
# configuration by leaving it unspecified, which works fine as long as
# people don't mess with Git's line ending settings, which is the vast
# majority of cases.
/packages/app-lib/migrations/20240711194701_init.sql !eol
/packages/app-lib/migrations/20240813205023_drop-active-unique.sql !eol
/packages/app-lib/migrations/20240930001852_disable-personalized-ads.sql !eol
/packages/app-lib/migrations/20241222013857_feature-flags.sql !eol

View File

@@ -18,9 +18,6 @@ on:
jobs:
docker:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./apps/labrinth
steps:
- name: Checkout
uses: actions/checkout@v2
@@ -38,8 +35,6 @@ jobs:
- name: Build and push
id: docker_build
uses: docker/build-push-action@v2
env:
SQLX_OFFLINE: true
with:
file: ./apps/labrinth/Dockerfile
push: ${{ github.event_name != 'pull_request' }}

150
.github/workflows/theseus-build.yml vendored Normal file
View File

@@ -0,0 +1,150 @@
name: Modrinth App build
on:
push:
branches:
- main
tags:
- 'v*'
paths:
- .github/workflows/theseus-build.yml
- 'apps/app/**'
- 'apps/app-frontend/**'
- 'packages/app-lib/**'
- 'packages/app-macros/**'
- 'packages/assets/**'
- 'packages/ui/**'
- 'packages/utils/**'
workflow_dispatch:
inputs:
sign-windows-binaries:
description: Sign Windows binaries
type: boolean
default: true
required: false
jobs:
build:
name: Build
strategy:
fail-fast: false
matrix:
platform: [macos-latest, windows-latest, ubuntu-22.04]
include:
- platform: macos-latest
artifact-target-name: universal-apple-darwin
- platform: windows-latest
artifact-target-name: x86_64-pc-windows-msvc
- platform: ubuntu-22.04
artifact-target-name: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.platform }}
steps:
- name: 📥 Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: 🧰 Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
rustflags: ''
target: ${{ startsWith(matrix.platform, 'macos') && 'x86_64-apple-darwin' || '' }}
- name: 🧰 Install pnpm
uses: pnpm/action-setup@v4
- name: 🧰 Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
cache: pnpm
- name: 🧰 Install Linux build dependencies
if: startsWith(matrix.platform, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -yq libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
- name: 🧰 Setup Dasel
uses: jaxxstorm/action-install-gh-release@v2.1.0
with:
repo: TomWright/dasel
tag: v2.8.1
extension-matching: disable
rename-to: ${{ startsWith(matrix.platform, 'windows') && 'dasel.exe' || 'dasel' }}
chmod: 0755
- name: ⚙️ Set application version
shell: bash
run: |
APP_VERSION="$(git describe --tags --always | sed -E 's/-([0-9]+)-(g[0-9a-fA-F]+)$/-canary+\1.\2/')"
echo "Setting application version to $APP_VERSION"
dasel put -f apps/app/Cargo.toml -t string -v "${APP_VERSION#v}" 'package.version'
dasel put -f packages/app-lib/Cargo.toml -t string -v "${APP_VERSION#v}" 'package.version'
dasel put -f apps/app-frontend/package.json -t string -v "${APP_VERSION#v}" 'version'
- name: 💨 Setup Turbo cache
uses: rharkor/caching-for-turbo@v1.8
- name: 🧰 Install dependencies
run: pnpm install
- name: ✍️ Set up Windows code signing
if: startsWith(matrix.platform, 'windows')
shell: bash
run: |
if [ '${{ startsWith(github.ref, 'refs/tags/v') || inputs.sign-windows-binaries }}' = 'true' ]; then
choco install jsign --ignore-dependencies # GitHub runners come with a global Java installation already
else
dasel delete -f apps/app/tauri-release.conf.json 'bundle.windows.signCommand'
fi
- name: 🔨 Build macOS app
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
if: startsWith(matrix.platform, 'macos')
env:
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: 🔨 Build Linux app
run: pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json
if: startsWith(matrix.platform, 'ubuntu')
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: 🔨 Build Windows app
run: |
[System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream
$env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD"
$env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis,updater'
Remove-Item -Path signer-client-cert.p12 -ErrorAction SilentlyContinue
if: startsWith(matrix.platform, 'windows')
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
DIGICERT_ONE_SIGNER_API_KEY: ${{ secrets.DIGICERT_ONE_SIGNER_API_KEY }}
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64 }}
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD }}
- name: 📤 Upload app bundles
uses: actions/upload-artifact@v4
with:
name: App bundle (${{ matrix.artifact-target-name }})
path: |
target/release/bundle/appimage/Modrinth App_*.AppImage*
target/release/bundle/deb/Modrinth App_*.deb*
target/release/bundle/rpm/Modrinth App-*.rpm*
target/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz*
target/universal-apple-darwin/release/bundle/dmg/Modrinth App_*.dmg*
target/release/bundle/nsis/Modrinth App_*-setup.exe*
target/release/bundle/nsis/Modrinth App_*-setup.nsis.zip*

View File

@@ -1,186 +1,118 @@
name: 'Modrinth App build'
name: Modrinth App release
on:
push:
branches:
- main
tags:
- 'v*'
paths:
- .github/workflows/theseus-release.yml
- 'apps/app/**'
- 'apps/app-frontend/**'
- 'packages/app-lib/**'
- 'packages/app-macros/**'
- 'packages/assets/**'
- 'packages/ui/**'
- 'packages/utils/**'
workflow_dispatch:
inputs:
sign-windows-binaries:
description: Sign Windows binaries
type: boolean
default: true
required: false
version-tag:
description: Version tag to release to the wide public
type: string
required: true
release-notes:
description: Release notes to include in the Tauri version manifest
default: A new release of the Modrinth App is available!
type: string
required: true
jobs:
build:
strategy:
fail-fast: false
matrix:
platform: [macos-latest, windows-latest, ubuntu-22.04]
release:
name: Release Modrinth App
runs-on: ubuntu-latest
runs-on: ${{ matrix.platform }}
env:
LINUX_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-unknown-linux-gnu)
WINDOWS_X64_BUNDLE_ARTIFACT_NAME: App bundle (x86_64-pc-windows-msvc)
MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME: App bundle (universal-apple-darwin)
LAUNCHER_FILES_BUCKET_BASE_URL: https://launcher-files.modrinth.com
steps:
- uses: actions/checkout@v4
- name: Rust setup (mac)
if: startsWith(matrix.platform, 'macos')
uses: actions-rust-lang/setup-rust-toolchain@v1
- name: 📥 Download Modrinth App artifacts
uses: dawidd6/action-download-artifact@v11
with:
rustflags: ''
target: x86_64-apple-darwin
workflow: theseus-build.yml
workflow_conclusion: success
event: push
branch: ${{ inputs.version-tag }}
use_unzip: true
- name: Rust setup
if: "!startsWith(matrix.platform, 'macos')"
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
rustflags: ''
- name: Setup rust cache
uses: actions/cache@v4
with:
path: |
target/**
!target/*/release/bundle/*/*.dmg
!target/*/release/bundle/*/*.app.tar.gz
!target/*/release/bundle/*/*.app.tar.gz.sig
!target/release/bundle/*/*.dmg
!target/release/bundle/*/*.app.tar.gz
!target/release/bundle/*/*.app.tar.gz.sig
!target/release/bundle/appimage/*.AppImage
!target/release/bundle/appimage/*.AppImage.tar.gz
!target/release/bundle/appimage/*.AppImage.tar.gz.sig
!target/release/bundle/deb/*.deb
!target/release/bundle/rpm/*.rpm
!target/release/bundle/msi/*.msi
!target/release/bundle/msi/*.msi.zip
!target/release/bundle/msi/*.msi.zip.sig
!target/release/bundle/nsis/*.exe
!target/release/bundle/nsis/*.nsis.zip
!target/release/bundle/nsis/*.nsis.zip.sig
key: ${{ runner.os }}-rust-target-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-rust-target-
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Install pnpm via corepack
shell: bash
run: |
corepack enable
corepack prepare --activate
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Setup pnpm cache
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: install dependencies (ubuntu only)
if: startsWith(matrix.platform, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev pkg-config libayatana-appindicator3-dev librsvg2-dev
- name: Install code signing client (Windows only)
if: startsWith(matrix.platform, 'windows')
run: choco install jsign --ignore-dependencies # GitHub runners come with a global Java installation already
- name: Install frontend dependencies
run: pnpm install
- name: Disable Windows code signing for non-final release builds
if: ${{ startsWith(matrix.platform, 'windows') && !startsWith(github.ref, 'refs/tags/v') && !inputs.sign-windows-binaries }}
run: |
jq 'del(.bundle.windows.signCommand)' apps/app/tauri-release.conf.json > apps/app/tauri-release.conf.json.new
Move-Item -Path apps/app/tauri-release.conf.json.new -Destination apps/app/tauri-release.conf.json -Force
- name: build app (macos)
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
if: startsWith(matrix.platform, 'macos')
- name: 🛠️ Generate version manifest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ENABLE_CODE_SIGNING: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: build app (Linux)
run: pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json
if: startsWith(matrix.platform, 'ubuntu')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: build app (Windows)
VERSION_TAG: ${{ inputs.version-tag }}
RELEASE_NOTES: ${{ inputs.release-notes }}
run: |
[System.Convert]::FromBase64String("$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64") | Set-Content -Path signer-client-cert.p12 -AsByteStream
$env:DIGICERT_ONE_SIGNER_CREDENTIALS = "$env:DIGICERT_ONE_SIGNER_API_KEY|$PWD\signer-client-cert.p12|$env:DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD"
$env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis,updater'
Remove-Item -Path signer-client-cert.p12
if: startsWith(matrix.platform, 'windows')
# Reference: https://tauri.app/plugin/updater/#server-support
jq -nc \
--arg versionTag "${VERSION_TAG#v}" \
--arg releaseNotes "$RELEASE_NOTES" \
--rawfile macOsAarch64UpdateArtifactSignature "${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz.sig" \
--rawfile macOsX64UpdateArtifactSignature "${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/macos/Modrinth App.app.tar.gz.sig" \
--rawfile linuxX64UpdateArtifactSignature "${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/appimage/Modrinth App_${VERSION_TAG#v}_amd64.AppImage.tar.gz.sig" \
--rawfile windowsX64UpdateArtifactSignature "${WINDOWS_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/nsis/Modrinth App_${VERSION_TAG#v}_x64-setup.nsis.zip.sig" \
'{
"version": $versionTag,
"notes": $releaseNotes,
"pub_date": now | todateiso8601,
"platforms": {
"darwin-aarch64": {
"signature": $macOsAarch64UpdateArtifactSignature,
"url": @uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/macos/\("Modrinth App.app.tar.gz")",
"install_urls": [@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/macos/\("Modrinth App_" + $versionTag + "_universal.dmg")"]
},
"darwin-x86_64": {
"signature": $macOsX64UpdateArtifactSignature,
"url": @uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/macos/\("Modrinth App.app.tar.gz")",
"install_urls": [@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/macos/\("Modrinth App_" + $versionTag + "_universal.dmg")"]
},
"linux-x86_64": {
"signature": $linuxX64UpdateArtifactSignature,
"url": @uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/linux/\("Modrinth App_" + $versionTag + "_amd64.AppImage.tar.gz")",
"install_urls": [
@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/linux/\("Modrinth App_" + $versionTag + "_amd64.deb")",
@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/linux/\("Modrinth App_" + $versionTag + "_amd64.AppImage")",
@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/linux/\("Modrinth App-" + $versionTag + "-1.x86_64.rpm")"
]
},
"windows-x86_64": {
"signature": $windowsX64UpdateArtifactSignature,
"url": @uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/windows/\("Modrinth App_" + $versionTag + "_x64-setup.nsis.zip")",
"install_urls": [@uri "${{ env.LAUNCHER_FILES_BUCKET_BASE_URL }}/versions/\($versionTag)/windows/\("Modrinth App_" + $versionTag + "_x64-setup.exe")"]
}
}
}' > updates.json
echo "Generated manifest for version ${VERSION_TAG}:"
cat updates.json
- name: 📤 Upload release artifacts
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
DIGICERT_ONE_SIGNER_API_KEY: ${{ secrets.DIGICERT_ONE_SIGNER_API_KEY }}
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_BASE64 }}
DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD: ${{ secrets.DIGICERT_ONE_SIGNER_CLIENT_CERTIFICATE_PASSWORD }}
VERSION_TAG: ${{ inputs.version-tag }}
AWS_ACCESS_KEY_ID: ${{ secrets.LAUNCHER_FILES_BUCKET_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.LAUNCHER_FILES_BUCKET_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ secrets.LAUNCHER_FILES_BUCKET_NAME }}
AWS_REGION: ${{ secrets.LAUNCHER_FILES_BUCKET_REGION }}
AWS_ENDPOINT_URL: ${{ secrets.LAUNCHER_FILES_BUCKET_ENDPOINT_URL }}
AWS_PAGER: ''
# Work around incompatible checksum behavior with some S3-like object storage providers,
# such as Cloudflare R2. See:
# - https://developers.cloudflare.com/r2/examples/aws/aws-cli/
# - https://developers.cloudflare.com/r2/examples/aws/aws-sdk-java/
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
run: |
for macosBundleType in 'macos' 'dmg'; do
aws s3 cp --recursive \
"${MACOS_UNIVERSAL_BUNDLE_ARTIFACT_NAME}/universal-apple-darwin/release/bundle/${macosBundleType}" \
"s3://${AWS_BUCKET}/versions/${VERSION_TAG#v}/macos"
done
- name: upload ${{ matrix.platform }}
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.platform }}
path: |
target/*/release/bundle/*/*.dmg
target/*/release/bundle/*/*.app.tar.gz
target/*/release/bundle/*/*.app.tar.gz.sig
target/release/bundle/*/*.dmg
target/release/bundle/*/*.app.tar.gz
target/release/bundle/*/*.app.tar.gz.sig
for linuxBundleType in 'appimage' 'deb' 'rpm'; do
aws s3 cp --recursive \
"${LINUX_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/${linuxBundleType}" \
"s3://${AWS_BUCKET}/versions/${VERSION_TAG#v}/linux"
done
target/release/bundle/*/*.AppImage
target/release/bundle/*/*.AppImage.tar.gz
target/release/bundle/*/*.AppImage.tar.gz.sig
target/release/bundle/*/*.deb
target/release/bundle/*/*.rpm
for windowsBundleType in 'nsis'; do
aws s3 cp --recursive \
"${WINDOWS_X64_BUNDLE_ARTIFACT_NAME}/release/bundle/${windowsBundleType}" \
"s3://${AWS_BUCKET}/versions/${VERSION_TAG#v}/windows"
done
target/release/bundle/msi/*.msi
target/release/bundle/msi/*.msi.zip
target/release/bundle/msi/*.msi.zip.sig
target/release/bundle/nsis/*.exe
target/release/bundle/nsis/*.nsis.zip
target/release/bundle/nsis/*.nsis.zip.sig
aws s3 cp updates.json "s3://${AWS_BUCKET}"

4
Cargo.lock generated
View File

@@ -8999,7 +8999,7 @@ dependencies = [
[[package]]
name = "theseus"
version = "0.10.0"
version = "1.0.0-local"
dependencies = [
"ariadne",
"async-compression",
@@ -9064,7 +9064,7 @@ dependencies = [
[[package]]
name = "theseus_gui"
version = "0.10.0"
version = "1.0.0-local"
dependencies = [
"chrono",
"daedalus",

View File

@@ -1,7 +1,7 @@
{
"name": "@modrinth/app-frontend",
"private": true,
"version": "0.10.0",
"version": "1.0.0-local",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -19,6 +19,7 @@ import {
SettingsIcon,
WorldIcon,
XIcon,
NewspaperIcon,
} from '@modrinth/assets'
import {
Avatar,
@@ -194,14 +195,16 @@ async function setupApp() {
.then((res) => {
if (res && res.articles) {
// Format expected by NewsArticleCard component.
news.value = res.articles.map((article) => ({
...article,
path: article.link,
thumbnail: article.thumbnail,
title: article.title,
summary: article.summary,
date: article.date,
}))
news.value = res.articles
.map((article) => ({
...article,
path: article.link,
thumbnail: article.thumbnail,
title: article.title,
summary: article.summary,
date: article.date,
}))
.slice(0, 4)
}
})
@@ -610,6 +613,11 @@ function handleAuxClick(e) {
:key="`news-${index}`"
:article="item"
/>
<ButtonStyled color="brand" size="large">
<a href="https://modrinth.com/news" target="_blank" class="my-4">
<NewspaperIcon /> View all news
</a>
</ButtonStyled>
</div>
</div>
</div>

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -172,7 +172,10 @@ onUnmounted(() => unlisten())
<div class="flex items-center col-span-3 gap-1 text-secondary font-semibold">
<TimerIcon />
<span class="text-sm">
Played {{ formatRelativeTime(dayjs(instance.last_played).toISOString()) }}
<template v-if="instance.last_played">
Played {{ formatRelativeTime(dayjs(instance.last_played).toISOString()) }}
</template>
<template v-else> Never played </template>
</span>
</div>
</div>

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

@@ -30,7 +30,7 @@ const getInstances = async () => {
return dateB - dateA
})
.slice(0, 4)
.slice(0, 3)
}
await getInstances()

View File

@@ -59,7 +59,7 @@ watch(
<div class="mt-4 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-extrabold text-contrast">Hide nametag</h2>
<p class="m-0 mt-1">Disables the nametag above your player on the skins page. page.</p>
<p class="m-0 mt-1">Disables the nametag above your player on the skins page.</p>
</div>
<Toggle id="hide-nametag-skins-page" v-model="settings.hide_nametag_skins_page" />
</div>

View File

@@ -1,8 +1,3 @@
<script lang="ts">
import capeModelUrl from '@/assets/models/cape.gltf?url'
import wideModelUrl from '@/assets/models/classic_player.gltf?url'
import slimModelUrl from '@/assets/models/slim_player.gltf?url'
</script>
<template>
<UploadSkinModal ref="uploadModal" />
<ModalWrapper ref="modal" @on-hide="resetState">
@@ -16,9 +11,6 @@ import slimModelUrl from '@/assets/models/slim_player.gltf?url'
<div class="max-h-[25rem] w-[16rem] min-w-[16rem] overflow-hidden relative">
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
<SkinPreviewRenderer
:slim-model-src="slimModelUrl"
:wide-model-src="wideModelUrl"
:cape-model-src="capeModelUrl"
:variant="variant"
:texture-src="previewSkin || ''"
:cape-src="selectedCapeTexture"

View File

@@ -10,9 +10,6 @@ import {
} from '@modrinth/ui'
import { CheckIcon, XIcon } from '@modrinth/assets'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import capeModelUrl from '@/assets/models/cape.gltf?url'
import wideModelUrl from '@/assets/models/classic_player.gltf?url'
import slimModelUrl from '@/assets/models/slim_player.gltf?url'
const modal = useTemplateRef('modal')
@@ -88,9 +85,6 @@ defineExpose({
<div class="absolute top-[-4rem] left-0 h-[32rem] w-[16rem] flex-shrink-0">
<SkinPreviewRenderer
v-if="currentSkinTexture"
:slim-model-src="slimModelUrl"
:wide-model-src="wideModelUrl"
:cape-model-src="capeModelUrl"
:cape-src="currentCapeTexture"
:texture-src="currentSkinTexture"
:variant="currentSkinVariant"

View File

@@ -1,4 +1,5 @@
<script setup lang="ts">
import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
import {
EyeIcon,
@@ -42,6 +43,7 @@ const emit = defineEmits<{
const props = defineProps<{
instance: GameInstance
last_played: Dayjs
}>()
const loadingModpack = ref(!!props.instance.linked_data)
@@ -147,12 +149,12 @@ onUnmounted(() => {
: null
"
class="w-fit shrink-0"
:class="{ 'cursor-help smart-clickable:allow-pointer-events': instance.last_played }"
:class="{ 'cursor-help smart-clickable:allow-pointer-events': last_played }"
>
<template v-if="instance.last_played">
<template v-if="last_played">
{{
formatMessage(commonMessages.playedLabel, {
time: formatRelativeTime(instance.last_played.toISOString()),
time: formatRelativeTime(last_played.toISOString?.()),
})
}}
</template>

View File

@@ -84,7 +84,7 @@ async function populateJumpBackIn() {
worldItems.push({
type: 'world',
last_played: dayjs(world.last_played),
last_played: dayjs(world.last_played ?? 0),
world: world,
instance: instance,
})
@@ -138,13 +138,13 @@ async function populateJumpBackIn() {
instanceItems.push({
type: 'instance',
last_played: dayjs(instance.last_played),
last_played: dayjs(instance.last_played ?? 0),
instance: instance,
})
}
const items: JumpBackInItem[] = [...worldItems, ...instanceItems]
items.sort((a, b) => dayjs(b.last_played).diff(dayjs(a.last_played)))
items.sort((a, b) => dayjs(b.last_played ?? 0).diff(dayjs(a.last_played ?? 0)))
jumpBackInItems.value = items
.filter((item, index) => index < MIN_JUMP_BACK_IN || item.last_played.isAfter(TWO_WEEKS_AGO))
.slice(0, MAX_JUMP_BACK_IN)
@@ -291,7 +291,7 @@ onUnmounted(() => {
"
@stop="() => stopInstance(item.instance.path)"
/>
<InstanceItem v-else :instance="item.instance" />
<InstanceItem v-else :instance="item.instance" :last_played="item.last_played" />
</template>
</div>
</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

@@ -4,9 +4,7 @@ import { get_normalized_skin_texture, determineModelType } from '../skins'
import { reactive } from 'vue'
import { setupSkinModel, disposeCaches } from '@modrinth/utils'
import { skinPreviewStorage } from '../storage/skin-preview-storage'
import capeModelUrl from '@/assets/models/cape.gltf?url'
import wideModelUrl from '@/assets/models/classic_player.gltf?url'
import slimModelUrl from '@/assets/models/slim_player.gltf?url'
import { CapeModel, ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
export interface RenderResult {
forwards: string
@@ -127,11 +125,11 @@ class BatchSkinRenderer {
function getModelUrlForVariant(variant: string): string {
switch (variant) {
case 'SLIM':
return slimModelUrl
return SlimPlayerModel
case 'CLASSIC':
case 'UNKNOWN':
default:
return wideModelUrl
return ClassicPlayerModel
}
}
@@ -281,6 +279,7 @@ async function generateHeadRender(skin: Skin): Promise<string> {
headMap.set(headKey, headUrl)
try {
// @ts-expect-error - skinPreviewStorage.store expects a RenderResult, but we are storing a string url.
await skinPreviewStorage.store(headKey, headUrl)
} catch (error) {
console.warn('Failed to store head render in persistent storage:', error)
@@ -335,7 +334,7 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
await get_normalized_skin_texture(skin),
modelUrl,
cape?.texture,
capeModelUrl,
CapeModel,
)
map.set(key, renderResult)

View File

@@ -97,7 +97,11 @@ export async function fixUnknownSkins(list: Skin[]) {
export function filterDefaultSkins(list: Skin[]) {
return list
.filter((s) => s.source === 'default' && (!s.name || s.variant === DEFAULT_MODELS[s.name]))
.filter(
(s) =>
s.source === 'default' &&
(!s.name || !(s.name in DEFAULT_MODELS) || s.variant === DEFAULT_MODELS[s.name]),
)
.sort((a, b) => {
const aIndex = a.name ? DEFAULT_MODEL_SORTING.indexOf(a.name) : -1
const bIndex = b.name ? DEFAULT_MODEL_SORTING.indexOf(b.name) : -1

View File

@@ -220,7 +220,7 @@ async function refreshSearch() {
}
}
results.value = rawResults.result
currentPage.value = Math.min(pageCount.value, currentPage.value)
currentPage.value = Math.max(1, Math.min(pageCount.value, currentPage.value))
const persistentParams: LocationQuery = {}

View File

@@ -43,10 +43,6 @@ import { handleSevereError } from '@/store/error'
import { trackEvent } from '@/helpers/analytics'
import type AccountsCard from '@/components/ui/AccountsCard.vue'
import { arrayBufferToBase64 } from '@modrinth/utils'
import capeModelUrl from '@/assets/models/cape.gltf?url'
import wideModelUrl from '@/assets/models/classic_player.gltf?url'
import slimModelUrl from '@/assets/models/slim_player.gltf?url'
const editSkinModal = useTemplateRef('editSkinModal')
const selectCapeModal = useTemplateRef('selectCapeModal')
const uploadSkinModal = useTemplateRef('uploadSkinModal')
@@ -320,9 +316,6 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
</h1>
<div class="preview-container">
<SkinPreviewRenderer
:wide-model-src="wideModelUrl"
:slim-model-src="slimModelUrl"
:cape-model-src="capeModelUrl"
:cape-src="capeTexture"
:texture-src="skinTexture || ''"
:variant="skinVariant"

View File

@@ -1,6 +1,6 @@
[package]
name = "theseus_gui"
version = "0.10.0"
version = "1.0.0-local" # The actual version is set by the theseus-build workflow on tagging
description = "The Modrinth App is a desktop application for managing your Minecraft mods"
license = "GPL-3.0-only"
repository = "https://github.com/modrinth/code/apps/app/"

View File

@@ -18,5 +18,25 @@
<string>A Minecraft mod wants to access your camera.</string>
<key>NSMicrophoneUsageDescription</key>
<string>A Minecraft mod wants to access your microphone.</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>asset.localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
<key>textures.minecraft.net</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
</dict>
</dict>
</dict>
</dict>
</plist>

View File

@@ -41,7 +41,7 @@
]
},
"productName": "Modrinth App",
"version": "0.10.0",
"version": "../app-frontend/package.json",
"mainBinaryName": "Modrinth App",
"identifier": "ModrinthApp",
"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,4 +1,4 @@
<script setup>
<script setup lang="ts">
import {
TrashIcon,
SearchIcon,
@@ -17,76 +17,134 @@ import LatestNewsRow from "~/components/ui/news/LatestNewsRow.vue";
import { homePageProjects } from "~/generated/state.json";
const os = ref(null);
const downloadWindows = ref(null);
const downloadLinux = ref(null);
const downloadSection = ref(null);
const windowsLink = ref(null);
const linuxLinks = {
appImage: null,
deb: null,
rpm: null,
thirdParty: "https://support.modrinth.com/en/articles/9298760",
};
const macLinks = {
universal: null,
};
interface LauncherPlatform {
install_urls: string[];
}
let downloadLauncher;
interface LauncherUpdates {
platforms: {
"darwin-aarch64": LauncherPlatform;
"windows-x86_64": LauncherPlatform;
"linux-x86_64": LauncherPlatform;
};
}
type OSType = "Mac" | "Windows" | "Linux" | null;
const downloadWindows = ref<HTMLAnchorElement | null>(null);
const downloadLinux = ref<HTMLAnchorElement | null>(null);
const downloadSection = ref<HTMLElement | null>(null);
const windowsLink = ref<string | null>(null);
const linuxLinks = reactive({
appImage: null as string | null,
deb: null as string | null,
rpm: null as string | null,
thirdParty: "https://support.modrinth.com/en/articles/9298760",
});
const macLinks = reactive({
universal: null as string | null,
});
const newProjects = homePageProjects.slice(0, 40);
const val = Math.ceil(newProjects.length / 6);
const rows = ref([
const rows = [
newProjects.slice(0, val),
newProjects.slice(val, val * 2),
newProjects.slice(val * 2, val * 3),
newProjects.slice(val * 3, val * 4),
newProjects.slice(val * 4, val * 5),
]);
];
const [{ data: launcherUpdates }] = await Promise.all([
await useAsyncData("launcherUpdates", () =>
$fetch("https://launcher-files.modrinth.com/updates.json"),
),
]);
const { data: launcherUpdates } = await useFetch<LauncherUpdates>(
"https://launcher-files.modrinth.com/updates.json?new",
{
server: false,
getCachedData(key, nuxtApp) {
const cached = (nuxtApp.ssrContext?.cache as any)?.[key] || nuxtApp.payload.data[key];
if (!cached) return;
macLinks.universal = launcherUpdates.value.platforms["darwin-aarch64"].install_urls[0];
windowsLink.value = launcherUpdates.value.platforms["windows-x86_64"].install_urls[0];
linuxLinks.appImage = launcherUpdates.value.platforms["linux-x86_64"].install_urls[1];
linuxLinks.deb = launcherUpdates.value.platforms["linux-x86_64"].install_urls[0];
linuxLinks.rpm = launcherUpdates.value.platforms["linux-x86_64"].install_urls[2];
const now = Date.now();
const cacheTime = cached._cacheTime || 0;
const maxAge = 5 * 60 * 1000;
onMounted(() => {
os.value = navigator?.platform.toString();
os.value = os.value?.includes("Mac")
? "Mac"
: os.value?.includes("Win")
? "Windows"
: os.value?.includes("Linux")
? "Linux"
: null;
if (now - cacheTime > maxAge) {
return null;
}
return cached;
},
transform(data) {
return {
...data,
_cacheTime: Date.now(),
};
},
},
);
const platform = computed<string>(() => {
if (import.meta.server) {
const headers = useRequestHeaders();
return headers["user-agent"] || "";
} else {
return navigator.userAgent || "";
}
});
const os = computed<OSType>(() => {
if (platform.value.includes("Mac")) {
return "Mac";
} else if (platform.value.includes("Win")) {
return "Windows";
} else if (platform.value.includes("Linux")) {
return "Linux";
} else {
return null;
}
});
const downloadLauncher = computed(() => {
if (os.value === "Windows") {
downloadLauncher = () => {
downloadWindows.value.click();
return () => {
downloadWindows.value?.click();
};
} else if (os.value === "Linux") {
downloadLauncher = () => {
downloadLinux.value.click();
return () => {
downloadLinux.value?.click();
};
} else {
downloadLauncher = () => {
return () => {
scrollToSection();
};
}
});
const handleDownload = () => {
downloadLauncher.value();
};
watch(
launcherUpdates,
(newData) => {
if (newData?.platforms) {
macLinks.universal = newData.platforms["darwin-aarch64"]?.install_urls[0] || null;
windowsLink.value = newData.platforms["windows-x86_64"]?.install_urls[0] || null;
linuxLinks.appImage = newData.platforms["linux-x86_64"]?.install_urls[1] || null;
linuxLinks.deb = newData.platforms["linux-x86_64"]?.install_urls[0] || null;
linuxLinks.rpm = newData.platforms["linux-x86_64"]?.install_urls[2] || null;
}
},
{ immediate: true },
);
const scrollToSection = () => {
nextTick(() => {
window.scrollTo({
top: downloadSection.value.offsetTop,
behavior: "smooth",
});
if (downloadSection.value) {
window.scrollTo({
top: downloadSection.value.offsetTop,
behavior: "smooth",
});
}
});
};
@@ -119,7 +177,7 @@ useSeoMeta({
v-if="os"
class="iconified-button brand-button btn btn-large"
rel="noopener nofollow"
@click="downloadLauncher"
@click="handleDownload"
>
<svg
v-if="os === 'Linux'"
@@ -485,7 +543,7 @@ useSeoMeta({
class="project button-animation gradient-border"
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}`"
>
<Avatar :src="project.icon_url" :alt="project.title" size="sm" loading="lazy" />
<Avatar :src="project.icon_url!" :alt="project.title" size="sm" />
<div class="project-info">
<span class="title">
{{ project.title }}
@@ -596,9 +654,7 @@ useSeoMeta({
</div>
<div class="description">
Modrinths launcher is fully open source. You can view the source code on our
<a href="https://github.com/modrinth/theseus" rel="noopener" :target="$external()"
>GitHub</a
>!
<a href="https://github.com/modrinth/theseus" rel="noopener" target="_blank">GitHub</a>!
</div>
</div>
<div class="point">
@@ -788,7 +844,7 @@ useSeoMeta({
Windows
</div>
<div class="description">
<a ref="downloadWindows" :href="windowsLink" download="">
<a ref="downloadWindows" :href="windowsLink || undefined" download="">
<DownloadIcon />
<span> Download the beta </span>
</a>
@@ -812,7 +868,7 @@ useSeoMeta({
Mac
</div>
<div class="description apple">
<a :href="macLinks.universal" download="">
<a :href="macLinks.universal || undefined" download="">
<DownloadIcon />
<span> Download the beta </span>
</a>
@@ -849,19 +905,19 @@ useSeoMeta({
Linux
</div>
<div class="description apple">
<a ref="downloadLinux" :href="linuxLinks.appImage" download="">
<a ref="downloadLinux" :href="linuxLinks.appImage || undefined" download="">
<DownloadIcon />
<span> Download the AppImage </span>
</a>
<a :href="linuxLinks.deb" download="">
<a :href="linuxLinks.deb || undefined" download="">
<DownloadIcon />
<span> Download the DEB </span>
</a>
<a :href="linuxLinks.rpm" download="">
<a :href="linuxLinks.rpm || undefined" download="">
<DownloadIcon />
<span> Download the RPM </span>
</a>
<a :href="linuxLinks.thirdParty" download="">
<a :href="linuxLinks.thirdParty || undefined" download="">
<LinkIcon />
<span> Third-party packages </span>
</a>

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

@@ -0,0 +1,6 @@
Contact: mailto:jai@modrinth.com
Expires: 2025-12-31T00:00:00.000Z
Preferred-Languages: en
Canonical: https://modrinth.com/.well-known/security.txt
Policy: https://modrinth.com/legal/security
Hiring: https://careers.modrinth.com/

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -1,5 +1,12 @@
{
"articles": [
{
"title": "Skins — Now in Modrinth App!",
"summary": "Customize your look, save your favorite skins, and swap them out in a flash, all within Modrinth App.",
"thumbnail": "https://modrinth.com/news/article/skins-now-in-modrinth-app/thumbnail.webp",
"date": "2025-07-06T23:45:00.000Z",
"link": "https://modrinth.com/news/article/skins-now-in-modrinth-app"
},
{
"title": "Creator Updates, July 2025",
"summary": "Addressing recent growth and growing pains that have been affecting creators.",
@@ -9,7 +16,7 @@
},
{
"title": "A Pride Month Success: Over $8,400 Raised for The Trevor Project!",
"summary": "A reflection on our Pride Month fundraiser campaign, which raised thousands for LGBTQ+ youth.",
"summary": "Reflecting on our Pride Month fundraiser campaign for LGBTQ+ youth.",
"thumbnail": "https://modrinth.com/news/article/pride-campaign-2025/thumbnail.webp",
"date": "2025-07-01T18:00:00.000Z",
"link": "https://modrinth.com/news/article/pride-campaign-2025"
@@ -107,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"
@@ -142,14 +149,14 @@
},
{
"title": "This week in Modrinth development: Filters and Fixes",
"summary": "After a great first week since Modrinth launched out of beta, we have continued to improve the user interface based on feedback.",
"summary": "Continuing to improve the user interface after a great first week since Modrinth launched out of beta.",
"thumbnail": "https://modrinth.com/news/article/knossos-v2.1.0/thumbnail.webp",
"date": "2022-03-09T00:00:00.000Z",
"link": "https://modrinth.com/news/article/knossos-v2.1.0"
},
{
"title": "Now showing on Modrinth: A new look!",
"summary": "After months of relatively quiet development, Modrinth has released many new features and improvements, including a redesign. Read on to learn more!",
"summary": "Releasing many new features and improvements, including a redesign!",
"thumbnail": "https://modrinth.com/news/article/redesign/thumbnail.webp",
"date": "2022-02-27T00:00:00.000Z",
"link": "https://modrinth.com/news/article/redesign"

View File

@@ -4,15 +4,23 @@
<description><![CDATA[Keep up-to-date on the latest news from Modrinth.]]></description>
<link>https://modrinth.com/news/</link>
<generator>@modrinth/blog</generator>
<lastBuildDate>Wed, 02 Jul 2025 02:42:05 GMT</lastBuildDate>
<lastBuildDate>Sat, 05 Jul 2025 21:04:25 GMT</lastBuildDate>
<atom:link href="https://modrinth.com/news/feed/rss.xml" rel="self" type="application/rss+xml"/>
<language><![CDATA[en]]></language>
<item>
<title><![CDATA[Skins — Now in Modrinth App!]]></title>
<description><![CDATA[Customize your look, save your favorite skins, and swap them out in a flash, all within Modrinth App.]]></description>
<link>https://modrinth.com/news/article/skins-now-in-modrinth-app/</link>
<guid isPermaLink="false">https://modrinth.com/news/article/skins-now-in-modrinth-app/</guid>
<pubDate>Sat, 05 Jul 2025 19:19:00 GMT</pubDate>
<content:encoded>&lt;![CDATA[&lt;p&gt;We&apos;re thrilled to roll out Modrinth App &lt;strong&gt;v0.10&lt;/strong&gt; with a beta release of one of our most highly requested features, the &lt;strong&gt;Skins page&lt;/strong&gt;. 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.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;./skins-page.webp&quot; alt=&quot;The new skins page, featuring a cute animated player model, your custom skins &amp;amp; default skins.&quot;&gt;&lt;/p&gt;&lt;p&gt;Adding a new skin is simple, even Herobrine could do it! When you add or edit a skin, you can &lt;strong&gt;upload&lt;/strong&gt; your custom texture file directly from your computer, &lt;strong&gt;choose&lt;/strong&gt; between the wide or slim arm style to match your preferred character model, and even &lt;strong&gt;assign&lt;/strong&gt; a specific cape to that look for the perfect finishing touch.&lt;/p&gt;&lt;p&gt;The interface makes it easy to preview your changes in real-time with the animated player model, so you can see exactly how your skin will look in-game before saving it.&lt;/p&gt;&lt;p&gt;&lt;img src=&quot;./edit-skin.webp&quot; alt=&quot;The edit skin modal that shows when you go to add or edit a skin.&quot;&gt;&lt;/p&gt;&lt;h2&gt;Fixes and More!&lt;/h2&gt;&lt;p&gt;Alongside this major new feature, &lt;strong&gt;v0.10&lt;/strong&gt; includes a host of improvements and bug fixes to make your experience smoother. We&apos;ve updated the news feed to use our new system, fixed issues with project descriptions, and tidied up how data is handled. For a full breakdown of all the changes, you can &lt;a href=&quot;https://modrinth.com/news/changelog?filter=app&quot;&gt;check out the complete changelog here.&lt;/a&gt;&lt;/p&gt;&lt;p&gt;As the skins feature is in &lt;em&gt;beta&lt;/em&gt;, we&apos;re eager to hear your feedback! &lt;strong&gt;Jump in, give it a try&lt;/strong&gt;, and let us know what you think. You can share your thoughts on our &lt;a href=&quot;https://discord.modrinth.com/&quot; rel=&quot;noopener nofollow ugc&quot;&gt;Discord server&lt;/a&gt; or &lt;a href=&quot;https://support.modrinth.com&quot; rel=&quot;noopener nofollow ugc&quot;&gt;start a support chat&lt;/a&gt; if you&apos;re running into issues.&lt;/p&gt;&lt;p&gt;Thank you! We can&apos;t wait to see your skins in action. Happy customizing!&lt;/p&gt;]]&gt;</content:encoded>
</item>
<item>
<title><![CDATA[Creator Updates, July 2025]]></title>
<description><![CDATA[Addressing recent growth and growing pains that have been affecting creators.]]></description>
<link>https://modrinth.com/news/article/creator-updates-july-2025/</link>
<guid isPermaLink="false">https://modrinth.com/news/article/creator-updates-july-2025/</guid>
<pubDate>Wed, 02 Jul 2025 03:00:00 GMT</pubDate>
<pubDate>Wed, 02 Jul 2025 04:20:00 GMT</pubDate>
<content:encoded>&lt;![CDATA[&lt;p&gt;Hey all,&lt;/p&gt;&lt;p&gt;The last few months have been quite hectic for Modrinth. We&apos;ve experienced all-time highs in both traffic and new creators and have outgrown a lot of our existing systems, which has led to a lot of issues plaguing creators, especially.&lt;/p&gt;&lt;p&gt;The team has been super hard at work at this, and I&apos;m really glad to announce that we&apos;ve fixed most of these issues long term.&lt;/p&gt;&lt;ol&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Upload issues (inputs not showing up, instability, etc)&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;We&apos;ve tracked these issues down to conflicting code between our ad provider and Modrinth&apos;s. For now, we&apos;ve &lt;strong&gt;disabled ads for all logged in users across the site&lt;/strong&gt; while we work on resolving these long term. Both web users and logged-in web users make a very small percentage of our ad revenue (7% for web and 0.05% for logged-in web users) so creators should see a very minimal revenue drop from this, and have a much better experience navigating and uploading to the site.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Moderation and report response times&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Creators have had to wait, in some cases, weeks to get their projects reviewed. This is unacceptable on our part and we are actively overhauling our moderation tooling to improve the moderation experience (and lowering time spent per project). We&apos;ve also hired 3 additional moderators/support staff (&lt;strong&gt;bringing our total to 7 and the total team to 17 people!&lt;/strong&gt;). We&apos;re hoping to see a significant reduction in queue times over the coming weeks.&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ad revenue instability&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;While ad revenue is generally out of our control and tends to fluctuate a lot, on June 4th we noticed a sharp decrease in creator revenue (~35% less than normal levels). While our ad provider initially thought this was a display issue, after further inquiry there were 2 causes: 1) Google AdExchange falsely flagging our traffic as invalid 2) Amazon banning many gaming publishers from their network &lt;a href=&quot;https://www.adweek.com/media/exclusive-ads-from-verizon-shell-and-others-ran-next-to-explicit-videos-on-top-android-app/&quot; rel=&quot;noopener nofollow ugc&quot;&gt;due to panic in the gaming ads space&lt;/a&gt;. While the Amazon ban is now resolved, we no longer are running Google AdExchange in the desktop app due to invalid traffic issues. This will lead to a permanent revenue decrease (AdX contributed to ~20% of our ad revenue). We also updated our prebid version (the underlying tech used to run ad auctions) which has shown a measurable increase, bringing revenue back to &amp;quot;normal&amp;quot; levels. Overall, we are closely monitoring and will keep you all posted. However, despite all the issues, due to some end-of-quarter campaigns, &lt;strong&gt;revenue in June was an all time high, at $227k ($170k paid to creators)&lt;/strong&gt;!&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Payout outages&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Creators should be able to withdraw their revenue at all times, but due to slow PayPal clearing times and poor planning by us, we&apos;ve had multiple week long outages in withdrawals. While we do store funds 1:1, these &amp;quot;outages&amp;quot; happen because we primarily store creator funds in an FDIC-insured bank account, as we wouldn&apos;t want a PayPal/Tremendous account suspension to cause creators to lose funds. We&apos;ve now set up internal reporting which should never cause this to happen again (or, if it does, drastically reduce the time payout outages happen)&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;Platform Revenue Route&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;Due to some unannounced breaking changes in Aditude&apos;s API, the platform revenue API was broken. It is now &lt;a href=&quot;https://api.modrinth.com/v3/payout/platform_revenue&quot; rel=&quot;noopener nofollow ugc&quot;&gt;working&lt;/a&gt;. You can also use &lt;code&gt;start&lt;/code&gt; and &lt;code&gt;end&lt;/code&gt; fields to filter any date range!&lt;/p&gt;&lt;/li&gt;&lt;li&gt;&lt;p&gt;&lt;strong&gt;API and Uptime&lt;/strong&gt;&lt;/p&gt;&lt;p&gt;We&apos;ve migrated our infrastructure for the website, app, and servers to OVH over our existing non-redundant AWS system. We&apos;ve hit 99.96% uptime on our API and 99.98% on Modrinth Servers!&lt;/p&gt;&lt;/li&gt;&lt;/ol&gt;&lt;p&gt;Thank you all for your patience! If you are having any more issues or have any questions about all of this, feel free to DM @geometrically on Discord or &lt;a href=&quot;https://support.modrinth.com&quot; rel=&quot;noopener nofollow ugc&quot;&gt;start a support chat&lt;/a&gt; and we will be happy to help!&lt;/p&gt;]]&gt;</content:encoded>
</item>
<item>

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,6 +1,6 @@
[package]
name = "theseus"
version = "0.10.0"
version = "1.0.0-local" # The actual version is set by the theseus-build workflow on tagging
authors = ["Jai A <jaiagr+gpg@pm.me>"]
edition.workspace = true

View File

@@ -2,6 +2,7 @@ package com.modrinth.theseus;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
@@ -76,14 +77,14 @@ public final class MinecraftLaunch {
Object thisObject = null;
if (!Modifier.isStatic(mainMethod.getModifiers())) {
thisObject = mainClass.getDeclaredConstructor().newInstance();
thisObject = forceAccessible(mainClass.getDeclaredConstructor()).newInstance();
}
final Object[] parameters = mainMethod.getParameterCount() > 0 ? new Object[] {args} : new Object[] {};
mainMethod.invoke(thisObject, parameters);
} else {
findSimpleMainMethod(mainClass).invoke(null, new Object[] {args});
forceAccessible(findSimpleMainMethod(mainClass)).invoke(null, new Object[] {args});
}
}
@@ -115,4 +116,15 @@ public final class MinecraftLaunch {
private static Method findSimpleMainMethod(Class<?> mainClass) throws NoSuchMethodException {
return mainClass.getMethod("main", String[].class);
}
private static <T extends AccessibleObject> T forceAccessible(T object) throws ReflectiveOperationException {
try {
final Method setAccessible0 = AccessibleObject.class.getDeclaredMethod("setAccessible0", boolean.class);
setAccessible0.setAccessible(true);
setAccessible0.invoke(object, true);
} catch (NoSuchMethodException e) {
object.setAccessible(true);
}
return object;
}
}

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

File diff suppressed because one or more lines are too long

View File

@@ -619,6 +619,12 @@ pub async fn launch_minecraft(
.into_iter(),
);
// The java launcher requires access to java.lang.reflect in order to force access in to
// whatever module the main class is in
if java_version.parsed_version >= 9 {
command.arg("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED");
}
// The java launcher code requires internal JDK code in Java 25+ in order to support JEP 512
if java_version.parsed_version >= 25 {
command.arg("--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED");

View File

@@ -1,7 +1,7 @@
module.exports = {
root: true,
extends: ['custom/library'],
ignorePatterns: ['**/*.scss', '**/*.svg', 'node_modules/', 'dist/'],
ignorePatterns: ['**/*.scss', '**/*.svg', 'node_modules/', 'dist/', '**/*.gltf'],
env: {
node: true,
},

View File

@@ -9,3 +9,8 @@ declare module '*.webp' {
const src: string
export default src
}
declare module '*?url' {
const src: string
export default src
}

View File

@@ -83,4 +83,8 @@ export const TwitterIcon = _TwitterIcon
export const WindowsIcon = _WindowsIcon
export const YouTubeIcon = _YouTubeIcon
export { default as CapeModel } from './models/cape.gltf?url'
export { default as ClassicPlayerModel } from './models/classic-player.gltf?url'
export { default as SlimPlayerModel } from './models/slim-player.gltf?url'
export * from './generated-icons'

View File

@@ -0,0 +1,92 @@
{
"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
}
]
}
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

@@ -0,0 +1,24 @@
---
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.
![The new skins page, featuring a cute animated player model, your custom skins & default skins.](./skins-page.webp)
Adding a new skin is simple, even Herobrine could do it! When you add or edit a skin, you can **upload** your custom texture file directly from your computer, **choose** between the wide or slim arm style to match your preferred character model, and even **assign** a specific cape to that look for the perfect finishing touch.
The interface makes it easy to preview your changes in real-time with the animated player model, so you can see exactly how your skin will look in-game before saving it.
![The edit skin modal that shows when you go to add or edit a skin.](./edit-skin.webp)
## Fixes and More!
Alongside this major new feature, **v0.10** includes a host of improvements and bug fixes to make your experience smoother. We've updated the news feed to use our new system, fixed issues with project descriptions, and tidied up how data is handled. For a full breakdown of all the changes, you can [check out the complete changelog here.](https://modrinth.com/news/changelog?filter=app)
As the skins feature is in _beta_, we're eager to hear your feedback! **Jump in, give it a try**, and let us know what you think. You can share your thoughts on our [Discord server](https://discord.modrinth.com/) or [start a support chat](https://support.modrinth.com) if you're running into issues.
Thank you! We can't wait to see your skins in action. Happy customizing!

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

@@ -20,6 +20,7 @@ import { article as new_site_beta } from './new_site_beta'
import { article as plugins_resource_packs } from './plugins_resource_packs'
import { article as pride_campaign_2025 } from './pride_campaign_2025'
import { article as redesign } from './redesign'
import { article as skins_now_in_modrinth_app } from './skins_now_in_modrinth_app'
import { article as two_years_of_modrinth_history } from './two_years_of_modrinth_history'
import { article as two_years_of_modrinth } from './two_years_of_modrinth'
import { article as whats_modrinth } from './whats_modrinth'
@@ -47,6 +48,7 @@ export const articles = [
plugins_resource_packs,
pride_campaign_2025,
redesign,
skins_now_in_modrinth_app,
two_years_of_modrinth_history,
two_years_of_modrinth,
whats_modrinth,

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.',

View File

@@ -6,5 +6,6 @@ export const article = {
'A small update with a big impact: plugins and resource packs are now available on Modrinth!',
date: '2022-08-27T00:00:00.000Z',
slug: 'plugins-resource-packs',
authors: ['6plzAzU4'],
thumbnail: true,
}

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