You've already forked AstralRinth
Compare commits
34 Commits
AR-0.10.3
...
14bf06e4bd
| Author | SHA1 | Date | |
|---|---|---|---|
| 14bf06e4bd | |||
|
|
cb72d2ac80 | ||
|
|
3c79607d1f | ||
| 8af0288274 | |||
| 167072de0c | |||
| 2df37be9a7 | |||
| 17cf5e3132 | |||
|
|
36ad1f16e4 | ||
|
|
5d4f334505 | ||
|
|
1fdb5ba748 | ||
| c5e67a5c6f | |||
| e2e21c1496 | |||
| 6da942ccbb | |||
|
|
26df6f51ef | ||
|
|
6caf794ae1 | ||
|
|
2692953e31 | ||
|
|
242fd713ab | ||
|
|
7a12c4d5e2 | ||
| 0ab4dec62d | |||
|
|
f256ef43c0 | ||
| 3ecb20afd6 | |||
| 1e10f24efe | |||
| 006fd7c7f5 | |||
| 1e8e001eb8 | |||
| 585935c799 | |||
|
|
e0cde2d6ff | ||
| a64c3360d2 | |||
|
|
e4e77dc0d2 | ||
|
|
8ba6467f21 | ||
| a2b2711204 | |||
|
|
088cb54317 | ||
| ab57926e44 | |||
| 35cd79727a | |||
|
|
c47bcf665d |
1
.dockerignore
Symbolic link
1
.dockerignore
Symbolic link
@@ -0,0 +1 @@
|
||||
.gitignore
|
||||
51
.github/workflows/astralrinth-build.yml
vendored
51
.github/workflows/astralrinth-build.yml
vendored
@@ -23,12 +23,13 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [macos-latest, windows-latest, ubuntu-latest]
|
||||
# platform: [macos-latest, windows-latest, ubuntu-latest]
|
||||
platform: [ubuntu-latest]
|
||||
include:
|
||||
- platform: macos-latest
|
||||
artifact-target-name: universal-apple-darwin
|
||||
- platform: windows-latest
|
||||
artifact-target-name: x86_64-pc-windows-msvc
|
||||
# - platform: macos-latest
|
||||
# artifact-target-name: universal-apple-darwin
|
||||
# - platform: windows-latest
|
||||
# artifact-target-name: x86_64-pc-windows-msvc
|
||||
- platform: ubuntu-latest
|
||||
artifact-target-name: x86_64-unknown-linux-gnu
|
||||
|
||||
@@ -72,11 +73,11 @@ jobs:
|
||||
- name: 🧰 Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: ✍️ Set up Windows code signing (jsign)
|
||||
if: matrix.platform == 'windows' && env.SIGN_WINDOWS_BINARIES == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
choco install jsign --ignore-dependencies
|
||||
# - name: ✍️ Set up Windows code signing (jsign)
|
||||
# if: matrix.platform == 'windows-latest' && env.SIGN_WINDOWS_BINARIES == 'true'
|
||||
# shell: bash
|
||||
# run: |
|
||||
# choco install jsign --ignore-dependencies
|
||||
|
||||
- name: 🗑️ Clean up cached bundles
|
||||
shell: bash
|
||||
@@ -84,12 +85,12 @@ jobs:
|
||||
rm -rf target/release/bundle
|
||||
rm -rf target/*/release/bundle || true
|
||||
|
||||
- name: 🔨 Build macOS app
|
||||
if: matrix.platform == 'macos-latest'
|
||||
run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
# - name: 🔨 Build macOS app
|
||||
# if: matrix.platform == 'macos-latest'
|
||||
# run: pnpm --filter=@modrinth/app run tauri build --target universal-apple-darwin --config tauri-release.conf.json
|
||||
# env:
|
||||
# TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
# TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: 🔨 Build Linux app
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
@@ -98,15 +99,15 @@ jobs:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: 🔨 Build Windows app
|
||||
if: matrix.platform == 'windows-latest'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
|
||||
pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis'
|
||||
env:
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
# - name: 🔨 Build Windows app
|
||||
# if: matrix.platform == 'windows-latest'
|
||||
# shell: pwsh
|
||||
# run: |
|
||||
# $env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
|
||||
# pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis'
|
||||
# env:
|
||||
# TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
# TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: 📤 Upload app bundles
|
||||
uses: actions/upload-artifact@v3
|
||||
|
||||
@@ -501,13 +501,13 @@ function handleAuxClick(e) {
|
||||
<!-- <ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" /> -->
|
||||
<div class="flex items-center gap-1 ml-3">
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
@click="router.back()"
|
||||
>
|
||||
<LeftArrowIcon />
|
||||
</button>
|
||||
<button
|
||||
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
|
||||
@click="router.forward()"
|
||||
>
|
||||
<RightArrowIcon />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
</div>
|
||||
<div v-if="updateState">
|
||||
<a>
|
||||
<Button class="download" :disabled="installState" @click="confirmUpdating(), getRemote(false, false)">
|
||||
<Button class="download" :disabled="installState" @click="initUpdateModal(), getRemote(false)">
|
||||
<DownloadIcon />
|
||||
{{
|
||||
installState
|
||||
@@ -48,7 +48,7 @@
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
<ModalWrapper ref="confirmUpdate" :has-to-type="false" header="Request to update the AstralRinth launcher">
|
||||
<ModalWrapper ref="updateModalView" :has-to-type="false" header="Request to update the AstralRinth launcher">
|
||||
<div class="modal-body">
|
||||
<div class="markdown-body">
|
||||
<p>The new version of the AstralRinth launcher is available.</p>
|
||||
@@ -66,14 +66,30 @@
|
||||
<p class="cosmic inline-fix">v{{ version }}</p>
|
||||
</span>
|
||||
<div class="button-group push-right">
|
||||
<Button class="download-modal" @click="confirmUpdate.hide()">
|
||||
Decline</Button>
|
||||
<Button class="download-modal" @click="approveUpdate()">
|
||||
Accept
|
||||
<Button class="updater-modal" @click="updateModalView.hide()">
|
||||
Cancel</Button>
|
||||
<Button class="updater-modal" @click="initDownload()">
|
||||
Download file
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
<ModalWrapper ref="updateRequestFailView" :has-to-type="false" header="Failed to request a file from the server :(">
|
||||
<div class="modal-body">
|
||||
<div class="markdown-body">
|
||||
<p><strong>Error occurred</strong></p>
|
||||
<p>Unfortunately, the program was unable to download the file from our servers.</p>
|
||||
<p>Please try downloading it yourself from <a href="https://me.astralium.su/get/ar" target="_blank" rel="noopener noreferrer">Git Astralium</a> if there are any updates available.</p>
|
||||
</div>
|
||||
<span>Local AstralRinth •
|
||||
<p class="cosmic inline-fix">v{{ version }}</p>
|
||||
</span>
|
||||
</div>
|
||||
<div class="button-group push-right">
|
||||
<Button class="updater-modal" @click="updateRequestFailView.hide()">
|
||||
Close</Button>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</div>
|
||||
<transition name="download">
|
||||
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
|
||||
@@ -129,18 +145,22 @@ const version = await getVersion()
|
||||
import { installState, getRemote, updateState } from '@/helpers/update.js'
|
||||
import ModalWrapper from './modal/ModalWrapper.vue'
|
||||
|
||||
const confirmUpdate = ref(null)
|
||||
const updateModalView = ref(null)
|
||||
const updateRequestFailView = ref(null)
|
||||
|
||||
const confirmUpdating = async () => {
|
||||
confirmUpdate.value.show()
|
||||
const initUpdateModal = async () => {
|
||||
updateModalView.value.show()
|
||||
}
|
||||
|
||||
const approveUpdate = async () => {
|
||||
confirmUpdate.value.hide()
|
||||
await getRemote(true, true)
|
||||
const initDownload = async () => {
|
||||
updateModalView.value.hide()
|
||||
const result = await getRemote(true);
|
||||
if (!result) {
|
||||
updateRequestFailView.value.show()
|
||||
}
|
||||
}
|
||||
|
||||
await getRemote(true, false)
|
||||
await getRemote(false)
|
||||
|
||||
const router = useRouter()
|
||||
const card = ref(null)
|
||||
@@ -375,7 +395,7 @@ onBeforeUnmount(() => {
|
||||
text-shadow: #26065e;
|
||||
}
|
||||
|
||||
.download-modal {
|
||||
.updater-modal {
|
||||
color: #3e8cde;
|
||||
padding: var(--gap-sm) var(--gap-lg);
|
||||
text-decoration: none;
|
||||
@@ -386,9 +406,9 @@ onBeforeUnmount(() => {
|
||||
transition: color 0.35s ease;
|
||||
}
|
||||
|
||||
.download-modal:hover,
|
||||
.download-modal:focus,
|
||||
.download-modal:active {
|
||||
.updater-modal:hover,
|
||||
.updater-modal:focus,
|
||||
.updater-modal:active {
|
||||
color: #10fae5;
|
||||
text-shadow: #26065e;
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ export async function get_jre(path) {
|
||||
|
||||
// Tests JRE version by running 'java -version' on it.
|
||||
// Returns true if the version is valid, and matches given (after extraction)
|
||||
export async function test_jre(path, majorVersion, minorVersion) {
|
||||
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion, minorVersion })
|
||||
export async function test_jre(path, majorVersion) {
|
||||
return await invoke('plugin:jre|jre_test_jre', { path, majorVersion })
|
||||
}
|
||||
|
||||
// Automatically installs specified java version
|
||||
|
||||
@@ -2,25 +2,40 @@ import * as THREE from 'three'
|
||||
import type { Skin, Cape } from '../skins'
|
||||
import { get_normalized_skin_texture, determineModelType } from '../skins'
|
||||
import { reactive } from 'vue'
|
||||
import { setupSkinModel, disposeCaches } from '@modrinth/utils'
|
||||
import { setupSkinModel, disposeCaches, loadTexture, applyCapeTexture } from '@modrinth/utils'
|
||||
import { skinPreviewStorage } from '../storage/skin-preview-storage'
|
||||
import { CapeModel, ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
||||
import { headStorage } from '../storage/head-storage'
|
||||
import { ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
||||
|
||||
export interface RenderResult {
|
||||
forwards: string
|
||||
backwards: string
|
||||
}
|
||||
|
||||
export interface RawRenderResult {
|
||||
forwards: Blob
|
||||
backwards: Blob
|
||||
}
|
||||
|
||||
class BatchSkinRenderer {
|
||||
private renderer: THREE.WebGLRenderer
|
||||
private readonly scene: THREE.Scene
|
||||
private readonly camera: THREE.PerspectiveCamera
|
||||
private renderer: THREE.WebGLRenderer | null = null
|
||||
private scene: THREE.Scene | null = null
|
||||
private camera: THREE.PerspectiveCamera | null = null
|
||||
private currentModel: THREE.Group | null = null
|
||||
private readonly width: number
|
||||
private readonly height: number
|
||||
|
||||
constructor(width: number = 360, height: number = 504) {
|
||||
this.width = width
|
||||
this.height = height
|
||||
}
|
||||
|
||||
private initializeRenderer(): void {
|
||||
if (this.renderer) return
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
canvas.width = this.width
|
||||
canvas.height = this.height
|
||||
|
||||
this.renderer = new THREE.WebGLRenderer({
|
||||
canvas: canvas,
|
||||
@@ -33,10 +48,10 @@ class BatchSkinRenderer {
|
||||
this.renderer.toneMapping = THREE.NoToneMapping
|
||||
this.renderer.toneMappingExposure = 10.0
|
||||
this.renderer.setClearColor(0x000000, 0)
|
||||
this.renderer.setSize(width, height)
|
||||
this.renderer.setSize(this.width, this.height)
|
||||
|
||||
this.scene = new THREE.Scene()
|
||||
this.camera = new THREE.PerspectiveCamera(20, width / height, 0.4, 1000)
|
||||
this.camera = new THREE.PerspectiveCamera(20, this.width / this.height, 0.4, 1000)
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||
@@ -50,9 +65,12 @@ class BatchSkinRenderer {
|
||||
textureUrl: string,
|
||||
modelUrl: string,
|
||||
capeUrl?: string,
|
||||
capeModelUrl?: string,
|
||||
): Promise<RenderResult> {
|
||||
await this.setupModel(modelUrl, textureUrl, capeModelUrl, capeUrl)
|
||||
): Promise<RawRenderResult> {
|
||||
this.initializeRenderer()
|
||||
|
||||
this.clearScene()
|
||||
|
||||
await this.setupModel(modelUrl, textureUrl, capeUrl)
|
||||
|
||||
const headPart = this.currentModel!.getObjectByName('Head')
|
||||
let lookAtTarget: [number, number, number]
|
||||
@@ -77,35 +95,32 @@ class BatchSkinRenderer {
|
||||
private async renderView(
|
||||
cameraPosition: [number, number, number],
|
||||
lookAtPosition: [number, number, number],
|
||||
): Promise<string> {
|
||||
): Promise<Blob> {
|
||||
if (!this.camera || !this.renderer || !this.scene) {
|
||||
throw new Error('Renderer not initialized')
|
||||
}
|
||||
|
||||
this.camera.position.set(...cameraPosition)
|
||||
this.camera.lookAt(...lookAtPosition)
|
||||
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
this.renderer.domElement.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob)
|
||||
resolve(url)
|
||||
} else {
|
||||
reject(new Error('Failed to create blob from canvas'))
|
||||
}
|
||||
}, 'image/png')
|
||||
})
|
||||
const dataUrl = this.renderer.domElement.toDataURL('image/webp', 0.9)
|
||||
const response = await fetch(dataUrl)
|
||||
return await response.blob()
|
||||
}
|
||||
|
||||
private async setupModel(
|
||||
modelUrl: string,
|
||||
textureUrl: string,
|
||||
capeModelUrl?: string,
|
||||
capeUrl?: string,
|
||||
): Promise<void> {
|
||||
if (this.currentModel) {
|
||||
this.scene.remove(this.currentModel)
|
||||
private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
|
||||
if (!this.scene) {
|
||||
throw new Error('Renderer not initialized')
|
||||
}
|
||||
|
||||
const { model } = await setupSkinModel(modelUrl, textureUrl, capeModelUrl, capeUrl)
|
||||
const { model } = await setupSkinModel(modelUrl, textureUrl)
|
||||
|
||||
if (capeUrl) {
|
||||
const capeTexture = await loadTexture(capeUrl)
|
||||
applyCapeTexture(model, capeTexture)
|
||||
}
|
||||
|
||||
const group = new THREE.Group()
|
||||
group.add(model)
|
||||
@@ -116,8 +131,39 @@ class BatchSkinRenderer {
|
||||
this.currentModel = group
|
||||
}
|
||||
|
||||
private clearScene(): void {
|
||||
if (!this.scene) return
|
||||
|
||||
while (this.scene.children.length > 0) {
|
||||
const child = this.scene.children[0]
|
||||
this.scene.remove(child)
|
||||
|
||||
if (child instanceof THREE.Mesh) {
|
||||
if (child.geometry) child.geometry.dispose()
|
||||
if (child.material) {
|
||||
if (Array.isArray(child.material)) {
|
||||
child.material.forEach((material) => material.dispose())
|
||||
} else {
|
||||
child.material.dispose()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
|
||||
directionalLight.castShadow = true
|
||||
directionalLight.position.set(2, 4, 3)
|
||||
this.scene.add(ambientLight)
|
||||
this.scene.add(directionalLight)
|
||||
|
||||
this.currentModel = null
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.renderer.dispose()
|
||||
if (this.renderer) {
|
||||
this.renderer.dispose()
|
||||
}
|
||||
disposeCaches()
|
||||
}
|
||||
}
|
||||
@@ -133,10 +179,25 @@ function getModelUrlForVariant(variant: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
export const map = reactive(new Map<string, RenderResult>())
|
||||
export const headMap = reactive(new Map<string, string>())
|
||||
export const skinBlobUrlMap = reactive(new Map<string, RenderResult>())
|
||||
export const headBlobUrlMap = reactive(new Map<string, string>())
|
||||
const DEBUG_MODE = false
|
||||
|
||||
let sharedRenderer: BatchSkinRenderer | null = null
|
||||
function getSharedRenderer(): BatchSkinRenderer {
|
||||
if (!sharedRenderer) {
|
||||
sharedRenderer = new BatchSkinRenderer()
|
||||
}
|
||||
return sharedRenderer
|
||||
}
|
||||
|
||||
export function disposeSharedRenderer(): void {
|
||||
if (sharedRenderer) {
|
||||
sharedRenderer.dispose()
|
||||
sharedRenderer = null
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
||||
const validKeys = new Set<string>()
|
||||
const validHeadKeys = new Set<string>()
|
||||
@@ -150,7 +211,7 @@ export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
||||
|
||||
try {
|
||||
await skinPreviewStorage.cleanupInvalidKeys(validKeys)
|
||||
await skinPreviewStorage.cleanupInvalidKeys(validHeadKeys)
|
||||
await headStorage.cleanupInvalidKeys(validHeadKeys)
|
||||
} catch (error) {
|
||||
console.warn('Failed to cleanup unused skin previews:', error)
|
||||
}
|
||||
@@ -229,13 +290,17 @@ export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64)
|
||||
outputCtx.drawImage(hatCanvas, 0, 0, 8, 8, 0, 0, size, size)
|
||||
}
|
||||
|
||||
outputCanvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(blob)
|
||||
} else {
|
||||
reject(new Error('Failed to create blob from canvas'))
|
||||
}
|
||||
}, 'image/png')
|
||||
outputCanvas.toBlob(
|
||||
(blob) => {
|
||||
if (blob) {
|
||||
resolve(blob)
|
||||
} else {
|
||||
reject(new Error('Failed to create blob from canvas'))
|
||||
}
|
||||
},
|
||||
'image/webp',
|
||||
0.9,
|
||||
)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
@@ -252,35 +317,24 @@ export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64)
|
||||
async function generateHeadRender(skin: Skin): Promise<string> {
|
||||
const headKey = `${skin.texture_key}-head`
|
||||
|
||||
if (headMap.has(headKey)) {
|
||||
if (headBlobUrlMap.has(headKey)) {
|
||||
if (DEBUG_MODE) {
|
||||
const url = headMap.get(headKey)!
|
||||
const url = headBlobUrlMap.get(headKey)!
|
||||
URL.revokeObjectURL(url)
|
||||
headMap.delete(headKey)
|
||||
headBlobUrlMap.delete(headKey)
|
||||
} else {
|
||||
return headMap.get(headKey)!
|
||||
return headBlobUrlMap.get(headKey)!
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const cached = await skinPreviewStorage.retrieve(headKey)
|
||||
if (cached && typeof cached === 'string') {
|
||||
headMap.set(headKey, cached)
|
||||
return cached
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to retrieve cached head render:', error)
|
||||
}
|
||||
|
||||
const skinUrl = await get_normalized_skin_texture(skin)
|
||||
const headBlob = await generatePlayerHeadBlob(skinUrl, 64)
|
||||
const headUrl = URL.createObjectURL(headBlob)
|
||||
|
||||
headMap.set(headKey, headUrl)
|
||||
headBlobUrlMap.set(headKey, headUrl)
|
||||
|
||||
try {
|
||||
// @ts-expect-error - skinPreviewStorage.store expects a RenderResult, but we are storing a string url.
|
||||
await skinPreviewStorage.store(headKey, headUrl)
|
||||
await headStorage.store(headKey, headBlob)
|
||||
} catch (error) {
|
||||
console.warn('Failed to store head render in persistent storage:', error)
|
||||
}
|
||||
@@ -293,30 +347,49 @@ export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
|
||||
}
|
||||
|
||||
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
|
||||
const renderer = new BatchSkinRenderer()
|
||||
|
||||
try {
|
||||
const skinKeys = skins.map(
|
||||
(skin) => `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`,
|
||||
)
|
||||
const headKeys = skins.map((skin) => `${skin.texture_key}-head`)
|
||||
|
||||
const [cachedSkinPreviews, cachedHeadPreviews] = await Promise.all([
|
||||
skinPreviewStorage.batchRetrieve(skinKeys),
|
||||
headStorage.batchRetrieve(headKeys),
|
||||
])
|
||||
|
||||
for (let i = 0; i < skins.length; i++) {
|
||||
const skinKey = skinKeys[i]
|
||||
const headKey = headKeys[i]
|
||||
|
||||
const rawCached = cachedSkinPreviews[skinKey]
|
||||
if (rawCached) {
|
||||
const cached: RenderResult = {
|
||||
forwards: URL.createObjectURL(rawCached.forwards),
|
||||
backwards: URL.createObjectURL(rawCached.backwards),
|
||||
}
|
||||
skinBlobUrlMap.set(skinKey, cached)
|
||||
}
|
||||
|
||||
const cachedHead = cachedHeadPreviews[headKey]
|
||||
if (cachedHead) {
|
||||
headBlobUrlMap.set(headKey, URL.createObjectURL(cachedHead))
|
||||
}
|
||||
}
|
||||
|
||||
for (const skin of skins) {
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
|
||||
if (map.has(key)) {
|
||||
if (skinBlobUrlMap.has(key)) {
|
||||
if (DEBUG_MODE) {
|
||||
const result = map.get(key)!
|
||||
const result = skinBlobUrlMap.get(key)!
|
||||
URL.revokeObjectURL(result.forwards)
|
||||
URL.revokeObjectURL(result.backwards)
|
||||
map.delete(key)
|
||||
skinBlobUrlMap.delete(key)
|
||||
} else continue
|
||||
}
|
||||
|
||||
try {
|
||||
const cached = await skinPreviewStorage.retrieve(key)
|
||||
if (cached) {
|
||||
map.set(key, cached)
|
||||
continue
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to retrieve cached skin preview:', error)
|
||||
}
|
||||
const renderer = getSharedRenderer()
|
||||
|
||||
let variant = skin.variant
|
||||
if (variant === 'UNKNOWN') {
|
||||
@@ -330,25 +403,35 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
|
||||
|
||||
const modelUrl = getModelUrlForVariant(variant)
|
||||
const cape: Cape | undefined = capes.find((_cape) => _cape.id === skin.cape_id)
|
||||
const renderResult = await renderer.renderSkin(
|
||||
const rawRenderResult = await renderer.renderSkin(
|
||||
await get_normalized_skin_texture(skin),
|
||||
modelUrl,
|
||||
cape?.texture,
|
||||
CapeModel,
|
||||
)
|
||||
|
||||
map.set(key, renderResult)
|
||||
const renderResult: RenderResult = {
|
||||
forwards: URL.createObjectURL(rawRenderResult.forwards),
|
||||
backwards: URL.createObjectURL(rawRenderResult.backwards),
|
||||
}
|
||||
|
||||
skinBlobUrlMap.set(key, renderResult)
|
||||
|
||||
try {
|
||||
await skinPreviewStorage.store(key, renderResult)
|
||||
await skinPreviewStorage.store(key, rawRenderResult)
|
||||
} catch (error) {
|
||||
console.warn('Failed to store skin preview in persistent storage:', error)
|
||||
}
|
||||
|
||||
await generateHeadRender(skin)
|
||||
const headKey = `${skin.texture_key}-head`
|
||||
if (!headBlobUrlMap.has(headKey)) {
|
||||
await generateHeadRender(skin)
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
renderer.dispose()
|
||||
disposeSharedRenderer()
|
||||
await cleanupUnusedPreviews(skins)
|
||||
|
||||
await skinPreviewStorage.debugCalculateStorage()
|
||||
await headStorage.debugCalculateStorage()
|
||||
}
|
||||
}
|
||||
|
||||
229
apps/app-frontend/src/helpers/storage/head-storage.ts
Normal file
229
apps/app-frontend/src/helpers/storage/head-storage.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
interface StoredHead {
|
||||
blob: Blob
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export class HeadStorage {
|
||||
private dbName = 'head-storage'
|
||||
private version = 1
|
||||
private db: IDBDatabase | null = null
|
||||
|
||||
async init(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, this.version)
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result
|
||||
resolve()
|
||||
}
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result
|
||||
if (!db.objectStoreNames.contains('heads')) {
|
||||
db.createObjectStore('heads')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async store(key: string, blob: Blob): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||
const store = transaction.objectStore('heads')
|
||||
|
||||
const storedHead: StoredHead = {
|
||||
blob,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.put(storedHead, key)
|
||||
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async retrieve(key: string): Promise<string | null> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||
const store = transaction.objectStore('heads')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.get(key)
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as StoredHead | undefined
|
||||
|
||||
if (!result) {
|
||||
resolve(null)
|
||||
return
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(result.blob)
|
||||
resolve(url)
|
||||
}
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async batchRetrieve(keys: string[]): Promise<Record<string, Blob | null>> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||
const store = transaction.objectStore('heads')
|
||||
const results: Record<string, Blob | null> = {}
|
||||
|
||||
return new Promise((resolve, _reject) => {
|
||||
let completedRequests = 0
|
||||
|
||||
if (keys.length === 0) {
|
||||
resolve(results)
|
||||
return
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const request = store.get(key)
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as StoredHead | undefined
|
||||
|
||||
if (result) {
|
||||
results[key] = result.blob
|
||||
} else {
|
||||
results[key] = null
|
||||
}
|
||||
|
||||
completedRequests++
|
||||
if (completedRequests === keys.length) {
|
||||
resolve(results)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
results[key] = null
|
||||
completedRequests++
|
||||
if (completedRequests === keys.length) {
|
||||
resolve(results)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||
const store = transaction.objectStore('heads')
|
||||
let deletedCount = 0
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.openCursor()
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||
|
||||
if (cursor) {
|
||||
const key = cursor.primaryKey as string
|
||||
|
||||
if (!validKeys.has(key)) {
|
||||
const deleteRequest = cursor.delete()
|
||||
deleteRequest.onsuccess = () => {
|
||||
deletedCount++
|
||||
}
|
||||
deleteRequest.onerror = () => {
|
||||
console.warn('Failed to delete invalid head entry:', key)
|
||||
}
|
||||
}
|
||||
|
||||
cursor.continue()
|
||||
} else {
|
||||
resolve(deletedCount)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async debugCalculateStorage(): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readonly')
|
||||
const store = transaction.objectStore('heads')
|
||||
|
||||
let totalSize = 0
|
||||
let count = 0
|
||||
const entries: Array<{ key: string; size: number }> = []
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.openCursor()
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||
|
||||
if (cursor) {
|
||||
const key = cursor.primaryKey as string
|
||||
const value = cursor.value as StoredHead
|
||||
|
||||
const entrySize = value.blob.size
|
||||
totalSize += entrySize
|
||||
count++
|
||||
|
||||
entries.push({
|
||||
key,
|
||||
size: entrySize,
|
||||
})
|
||||
|
||||
cursor.continue()
|
||||
} else {
|
||||
console.group('🗄️ Head Storage Debug Info')
|
||||
console.log(`Total entries: ${count}`)
|
||||
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
|
||||
console.log(
|
||||
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
|
||||
)
|
||||
|
||||
if (entries.length > 0) {
|
||||
const sortedEntries = entries.sort((a, b) => b.size - a.size)
|
||||
console.log(
|
||||
'Largest entry:',
|
||||
sortedEntries[0].key,
|
||||
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
|
||||
)
|
||||
console.log(
|
||||
'Smallest entry:',
|
||||
sortedEntries[sortedEntries.length - 1].key,
|
||||
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
|
||||
)
|
||||
}
|
||||
|
||||
console.groupEnd()
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async clearAll(): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['heads'], 'readwrite')
|
||||
const store = transaction.objectStore('heads')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.clear()
|
||||
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const headStorage = new HeadStorage()
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { RenderResult } from '../rendering/batch-skin-renderer'
|
||||
import type { RawRenderResult } from '../rendering/batch-skin-renderer'
|
||||
|
||||
interface StoredPreview {
|
||||
forwards: Blob
|
||||
@@ -30,18 +30,15 @@ export class SkinPreviewStorage {
|
||||
})
|
||||
}
|
||||
|
||||
async store(key: string, result: RenderResult): Promise<void> {
|
||||
async store(key: string, result: RawRenderResult): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const forwardsBlob = await fetch(result.forwards).then((r) => r.blob())
|
||||
const backwardsBlob = await fetch(result.backwards).then((r) => r.blob())
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readwrite')
|
||||
const store = transaction.objectStore('previews')
|
||||
|
||||
const storedPreview: StoredPreview = {
|
||||
forwards: forwardsBlob,
|
||||
backwards: backwardsBlob,
|
||||
forwards: result.forwards,
|
||||
backwards: result.backwards,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
|
||||
@@ -53,7 +50,7 @@ export class SkinPreviewStorage {
|
||||
})
|
||||
}
|
||||
|
||||
async retrieve(key: string): Promise<RenderResult | null> {
|
||||
async retrieve(key: string): Promise<RawRenderResult | null> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||
@@ -70,14 +67,56 @@ export class SkinPreviewStorage {
|
||||
return
|
||||
}
|
||||
|
||||
const forwards = URL.createObjectURL(result.forwards)
|
||||
const backwards = URL.createObjectURL(result.backwards)
|
||||
resolve({ forwards, backwards })
|
||||
resolve({ forwards: result.forwards, backwards: result.backwards })
|
||||
}
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async batchRetrieve(keys: string[]): Promise<Record<string, RawRenderResult | null>> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||
const store = transaction.objectStore('previews')
|
||||
const results: Record<string, RawRenderResult | null> = {}
|
||||
|
||||
return new Promise((resolve, _reject) => {
|
||||
let completedRequests = 0
|
||||
|
||||
if (keys.length === 0) {
|
||||
resolve(results)
|
||||
return
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
const request = store.get(key)
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as StoredPreview | undefined
|
||||
|
||||
if (result) {
|
||||
results[key] = { forwards: result.forwards, backwards: result.backwards }
|
||||
} else {
|
||||
results[key] = null
|
||||
}
|
||||
|
||||
completedRequests++
|
||||
if (completedRequests === keys.length) {
|
||||
resolve(results)
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => {
|
||||
results[key] = null
|
||||
completedRequests++
|
||||
if (completedRequests === keys.length) {
|
||||
resolve(results)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
@@ -113,6 +152,67 @@ export class SkinPreviewStorage {
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
async debugCalculateStorage(): Promise<void> {
|
||||
if (!this.db) await this.init()
|
||||
|
||||
const transaction = this.db!.transaction(['previews'], 'readonly')
|
||||
const store = transaction.objectStore('previews')
|
||||
|
||||
let totalSize = 0
|
||||
let count = 0
|
||||
const entries: Array<{ key: string; size: number }> = []
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = store.openCursor()
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
|
||||
|
||||
if (cursor) {
|
||||
const key = cursor.primaryKey as string
|
||||
const value = cursor.value as StoredPreview
|
||||
|
||||
const entrySize = value.forwards.size + value.backwards.size
|
||||
totalSize += entrySize
|
||||
count++
|
||||
|
||||
entries.push({
|
||||
key,
|
||||
size: entrySize,
|
||||
})
|
||||
|
||||
cursor.continue()
|
||||
} else {
|
||||
console.group('🗄️ Skin Preview Storage Debug Info')
|
||||
console.log(`Total entries: ${count}`)
|
||||
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
|
||||
console.log(
|
||||
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
|
||||
)
|
||||
|
||||
if (entries.length > 0) {
|
||||
const sortedEntries = entries.sort((a, b) => b.size - a.size)
|
||||
console.log(
|
||||
'Largest entry:',
|
||||
sortedEntries[0].key,
|
||||
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
|
||||
)
|
||||
console.log(
|
||||
'Smallest entry:',
|
||||
sortedEntries[sortedEntries.length - 1].key,
|
||||
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
|
||||
)
|
||||
}
|
||||
|
||||
console.groupEnd()
|
||||
resolve()
|
||||
}
|
||||
}
|
||||
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const skinPreviewStorage = new SkinPreviewStorage()
|
||||
|
||||
@@ -2,21 +2,19 @@ import { ref } from 'vue'
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
import { getArtifact, getOS } from '@/helpers/utils.js'
|
||||
|
||||
|
||||
export const allowState = ref(false)
|
||||
export const installState = ref(false)
|
||||
export const updateState = ref(false)
|
||||
export const latestBetaCommitTruncatedSha = ref('')
|
||||
export const latestBetaCommitLink = ref('')
|
||||
export const launcherUrl = 'https://www.astralium.su/get/ar'
|
||||
|
||||
const os = ref('')
|
||||
const currentOS = ref('')
|
||||
const releaseLink = `https://git.astralium.su/api/v1/repos/didirus/AstralRinth/releases/latest`
|
||||
const failedFetch = [`Failed to fetch remote releases:`, `Failed to fetch remote commits:`]
|
||||
const osNames = ['macos', 'windows', 'linux']
|
||||
const macExtension = `.dmg` // MacOS file type for download
|
||||
const windowsExtension = `.msi` // Windows file type for download
|
||||
const blacklistedBuilds = [
|
||||
|
||||
const osList = ['macos', 'windows', 'linux']
|
||||
const macExtensionList = ['.app', '.dmg']
|
||||
const windowsExtensionList = ['.exe', '.msi']
|
||||
|
||||
const blacklistPrefixes = [
|
||||
`dev`,
|
||||
`nightly`,
|
||||
`dirty`,
|
||||
@@ -26,110 +24,73 @@ const blacklistedBuilds = [
|
||||
`dirty_nightly`,
|
||||
] // This is blacklisted builds for download. For example, file.startsWith('dev') is not allowed.
|
||||
|
||||
/**
|
||||
* Asynchronous function to get remote data and handle updates and downloads.
|
||||
*
|
||||
* @param {boolean} elementIdBool - Indicates whether to disable an element ID.
|
||||
* @param {boolean} downloadArtifactBool - Indicates whether to download an artifact.
|
||||
*/
|
||||
export async function getRemote(elementIdBool, downloadArtifactBool) {
|
||||
fetch(releaseLink)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(response.status)
|
||||
}
|
||||
return response.json()
|
||||
})
|
||||
.then(async (data) => {
|
||||
os.value = await getOS()
|
||||
const latestRelease = data.name
|
||||
let remoteVersion = undefined
|
||||
export async function getRemote(isDownloadState) {
|
||||
var releaseData = null;
|
||||
var result = false;
|
||||
try {
|
||||
const response = await fetch(releaseLink);
|
||||
if (!response.ok) {
|
||||
throw new Error(response.status);
|
||||
}
|
||||
const remoteData = await response.json();
|
||||
currentOS.value = await getOS();
|
||||
const remoteLatestReleaseTag = remoteData.tag_name;
|
||||
releaseData = document.getElementById('releaseData');
|
||||
const remoteVersion = releaseData ? (releaseData.textContent = remoteLatestReleaseTag) : remoteLatestReleaseTag;
|
||||
|
||||
if (!elementIdBool) {
|
||||
const releaseData = document.getElementById('releaseData')
|
||||
if (releaseData == null) {
|
||||
console.error('Release data element not found.')
|
||||
return false
|
||||
}
|
||||
releaseData.textContent = latestRelease
|
||||
remoteVersion = `${releaseData.textContent}`
|
||||
} else {
|
||||
remoteVersion = latestRelease
|
||||
}
|
||||
if (osNames.includes(os.value.toLowerCase())) {
|
||||
if (remoteVersion.startsWith('v' + await getVersion())) {
|
||||
updateState.value = false
|
||||
allowState.value = false
|
||||
} else {
|
||||
updateState.value = true
|
||||
allowState.value = true
|
||||
}
|
||||
} else {
|
||||
updateState.value = false
|
||||
allowState.value = false
|
||||
}
|
||||
console.log('Update available state is', updateState.value)
|
||||
console.log('Remote version is', remoteVersion)
|
||||
console.log('Local version is', await getVersion())
|
||||
console.log('Operating System is', os.value)
|
||||
if (osList.includes(currentOS.value.toLowerCase())) {
|
||||
const localVersion = await getVersion();
|
||||
const isUpdateAvailable = !remoteVersion.includes(localVersion);
|
||||
|
||||
if (downloadArtifactBool) {
|
||||
installState.value = true
|
||||
const builds = data.assets
|
||||
const fileName = getInstaller(getExtension(), builds)
|
||||
if (fileName != null) {
|
||||
await getArtifact(fileName[1], fileName[0], os.value, true)
|
||||
}
|
||||
installState.value = false
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(failedFetch[0], error)
|
||||
if (!elementIdBool) {
|
||||
const errorData = document.getElementById('releaseData')
|
||||
if (errorData) {
|
||||
errorData.textContent = `${error.message}`
|
||||
}
|
||||
updateState.value = false
|
||||
allowState.value = false
|
||||
installState.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
updateState.value = isUpdateAvailable;
|
||||
allowState.value = isUpdateAvailable;
|
||||
} else {
|
||||
updateState.value = false;
|
||||
allowState.value = false;
|
||||
}
|
||||
if (isDownloadState) {
|
||||
installState.value = true;
|
||||
const builds = remoteData.assets;
|
||||
const fileName = getInstaller(getExtension(), builds);
|
||||
result = fileName ? await getArtifact(fileName[1], fileName[0], currentOS.value, true) : false;
|
||||
installState.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the installer for a specific operating system.
|
||||
*
|
||||
* @param {string} osExtension - The file extension of the installer.
|
||||
* @param {Array} builds - The list of builds.
|
||||
* @return {Array|null} An array containing the installer name and URL if found, or null if not found.
|
||||
*/
|
||||
function getInstaller(osExtension, builds) {
|
||||
for (let i of builds) {
|
||||
let blacklistedItem = false
|
||||
blacklistedBuilds.forEach((item) => {
|
||||
if (i.name.startsWith(item)) {
|
||||
return (blacklistedItem = true)
|
||||
console.log('Update available state is', updateState.value);
|
||||
console.log('Remote version is', remoteVersion);
|
||||
console.log('Local version is', await getVersion());
|
||||
console.log('Operating System is', currentOS.value);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(failedFetch[0], error);
|
||||
if (!releaseData) {
|
||||
const errorData = document.getElementById('releaseData');
|
||||
if (errorData) {
|
||||
errorData.textContent = `${error.message}`;
|
||||
}
|
||||
})
|
||||
if (i.name.endsWith(osExtension) && !blacklistedItem) {
|
||||
console.log(i.browser_download_url)
|
||||
return [i.name, i.browser_download_url]
|
||||
updateState.value = false;
|
||||
allowState.value = false;
|
||||
installState.value = false;
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to get the extension based on the operating system.
|
||||
*
|
||||
* @return {string} The extension based on the operating system.
|
||||
*/
|
||||
function getExtension() {
|
||||
if (os.value.toLowerCase() == osNames[0]) {
|
||||
return macExtension
|
||||
} else if (os.value.toLowerCase() == osNames[1]) {
|
||||
return windowsExtension
|
||||
function getInstaller(osExtension, builds) {
|
||||
console.log(osExtension, builds)
|
||||
for (const build of builds) {
|
||||
if (blacklistPrefixes.some(prefix => build.name.startsWith(prefix))) {
|
||||
continue;
|
||||
}
|
||||
if (osExtension.some(ext => build.name.endsWith(ext))) {
|
||||
console.log(build.name, build.browser_download_url);
|
||||
return [build.name, build.browser_download_url];
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getExtension() {
|
||||
return osList.find(osName => osName === currentOS.value.toLowerCase())?.endsWith('macos')
|
||||
? macExtensionList
|
||||
: windowsExtensionList;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
import { get as getSettings } from '@/helpers/settings.ts'
|
||||
import { get_default_user, login as login_flow, users } from '@/helpers/auth'
|
||||
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import { generateSkinPreviews, map } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import { generateSkinPreviews, skinBlobUrlMap } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import { handleSevereError } from '@/store/error'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import type AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
@@ -215,7 +215,7 @@ async function loadCurrentUser() {
|
||||
|
||||
function getBakedSkinTextures(skin: Skin): RenderResult | undefined {
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
return map.get(key)
|
||||
return skinBlobUrlMap.get(key)
|
||||
}
|
||||
|
||||
async function login() {
|
||||
|
||||
@@ -483,7 +483,7 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
height: calc(100vh - 11rem);
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
|
||||
@@ -34,9 +34,6 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
||||
|
||||
// let update_fut = updater.check();
|
||||
|
||||
// tracing::info!("Initializing app state...");
|
||||
State::init().await?;
|
||||
|
||||
// let check_bar = theseus::init_loading(
|
||||
// theseus::LoadingBarType::CheckingForUpdates,
|
||||
// 1.0,
|
||||
@@ -87,7 +84,7 @@ async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
|
||||
// #[cfg(not(feature = "updater"))]
|
||||
// {
|
||||
// }
|
||||
|
||||
tracing::info!("Initializing app state...");
|
||||
State::init().await?;
|
||||
tracing::info!("AstralRinth state successfully initialized.");
|
||||
let state = State::get().await?;
|
||||
@@ -164,10 +161,10 @@ fn main() {
|
||||
|
||||
let mut builder = tauri::Builder::default();
|
||||
|
||||
#[cfg(feature = "updater")]
|
||||
{
|
||||
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||
}
|
||||
// #[cfg(feature = "updater")]
|
||||
// {
|
||||
// builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||
// }
|
||||
|
||||
builder = builder
|
||||
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
]
|
||||
},
|
||||
"productName": "AstralRinth App",
|
||||
"version": "0.10.3",
|
||||
"version": "0.10.302",
|
||||
"mainBinaryName": "AstralRinth App",
|
||||
"identifier": "AstralRinthApp",
|
||||
"plugins": {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
FROM rust:1.88.0 AS build
|
||||
ENV PKG_CONFIG_ALLOW_CROSS=1
|
||||
|
||||
WORKDIR /usr/src/daedalus
|
||||
COPY . .
|
||||
@@ -10,11 +9,8 @@ FROM debian:bookworm-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates openssl \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN update-ca-certificates
|
||||
|
||||
COPY --from=build /usr/src/daedalus/target/release/daedalus_client /daedalus/daedalus_client
|
||||
WORKDIR /daedalus_client
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -22,26 +22,49 @@ export class FSModule extends ServerModule {
|
||||
this.opsQueuedForModification = [];
|
||||
}
|
||||
|
||||
private async retryWithAuth<T>(requestFn: () => Promise<T>): Promise<T> {
|
||||
private async retryWithAuth<T>(
|
||||
requestFn: () => Promise<T>,
|
||||
ignoreFailure: boolean = false,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await requestFn();
|
||||
} catch (error) {
|
||||
if (error instanceof ModrinthServerError && error.statusCode === 401) {
|
||||
console.debug("Auth failed, refreshing JWT and retrying");
|
||||
await this.fetch(); // Refresh auth
|
||||
return await requestFn();
|
||||
}
|
||||
|
||||
const available = await this.server.testNodeReachability();
|
||||
if (!available && !ignoreFailure) {
|
||||
this.server.moduleErrors.general = {
|
||||
error: new ModrinthServerError(
|
||||
"Unable to reach node. FS operation failed and subsequent ping test failed.",
|
||||
500,
|
||||
error as Error,
|
||||
"fs",
|
||||
),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
listDirContents(path: string, page: number, pageSize: number): Promise<DirectoryResponse> {
|
||||
listDirContents(
|
||||
path: string,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
ignoreFailure: boolean = false,
|
||||
): Promise<DirectoryResponse> {
|
||||
return this.retryWithAuth(async () => {
|
||||
const encodedPath = encodeURIComponent(path);
|
||||
return await useServersFetch(`/list?path=${encodedPath}&page=${page}&page_size=${pageSize}`, {
|
||||
override: this.auth,
|
||||
retry: false,
|
||||
});
|
||||
});
|
||||
}, ignoreFailure);
|
||||
}
|
||||
|
||||
createFileOrFolder(path: string, type: "file" | "directory"): Promise<void> {
|
||||
@@ -150,7 +173,7 @@ export class FSModule extends ServerModule {
|
||||
});
|
||||
}
|
||||
|
||||
downloadFile(path: string, raw?: boolean): Promise<any> {
|
||||
downloadFile(path: string, raw: boolean = false, ignoreFailure: boolean = false): Promise<any> {
|
||||
return this.retryWithAuth(async () => {
|
||||
const encodedPath = encodeURIComponent(path);
|
||||
const fileData = await useServersFetch(`/download?path=${encodedPath}`, {
|
||||
@@ -161,7 +184,7 @@ export class FSModule extends ServerModule {
|
||||
return raw ? fileData : await fileData.text();
|
||||
}
|
||||
return fileData;
|
||||
});
|
||||
}, ignoreFailure);
|
||||
}
|
||||
|
||||
extractFile(
|
||||
|
||||
@@ -46,13 +46,18 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
|
||||
data.image = (await this.server.processImage(data.project?.icon_url)) ?? undefined;
|
||||
}
|
||||
|
||||
const motd = await this.getMotd();
|
||||
if (motd === "A Minecraft Server") {
|
||||
await this.setMotd(
|
||||
`§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`,
|
||||
);
|
||||
try {
|
||||
const motd = await this.getMotd();
|
||||
if (motd === "A Minecraft Server") {
|
||||
await this.setMotd(
|
||||
`§b${data.project?.title || data.loader + " " + data.mc_version} §f♦ §aModrinth Servers`,
|
||||
);
|
||||
}
|
||||
data.motd = motd;
|
||||
} catch {
|
||||
console.error("[Modrinth Servers] [General] Failed to fetch MOTD.");
|
||||
data.motd = undefined;
|
||||
}
|
||||
data.motd = motd;
|
||||
|
||||
// Copy data to this module
|
||||
Object.assign(this, data);
|
||||
@@ -178,7 +183,7 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
|
||||
|
||||
async getMotd(): Promise<string | undefined> {
|
||||
try {
|
||||
const props = await this.server.fs.downloadFile("/server.properties");
|
||||
const props = await this.server.fs.downloadFile("/server.properties", false, true);
|
||||
if (props) {
|
||||
const lines = props.split("\n");
|
||||
for (const line of lines) {
|
||||
|
||||
@@ -42,6 +42,23 @@ export async function useServersFetch<T>(
|
||||
retry = method === "GET" ? 3 : 0,
|
||||
} = options;
|
||||
|
||||
const circuitBreakerKey = `${module || "default"}_${path}`;
|
||||
const failureCount = useState<number>(`fetch_failures_${circuitBreakerKey}`, () => 0);
|
||||
const lastFailureTime = useState<number>(`last_failure_${circuitBreakerKey}`, () => 0);
|
||||
|
||||
const now = Date.now();
|
||||
if (failureCount.value >= 3 && now - lastFailureTime.value < 30000) {
|
||||
const error = new ModrinthServersFetchError(
|
||||
"[Modrinth Servers] Circuit breaker open - too many recent failures",
|
||||
503,
|
||||
);
|
||||
throw new ModrinthServerError("Service temporarily unavailable", 503, error, module);
|
||||
}
|
||||
|
||||
if (now - lastFailureTime.value > 30000) {
|
||||
failureCount.value = 0;
|
||||
}
|
||||
|
||||
const base = (import.meta.server ? config.pyroBaseUrl : config.public.pyroBaseUrl)?.replace(
|
||||
/\/$/,
|
||||
"",
|
||||
@@ -69,6 +86,7 @@ export async function useServersFetch<T>(
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"User-Agent": "Modrinth/1.0 (https://modrinth.com)",
|
||||
"X-Archon-Request": "true",
|
||||
Vary: "Accept, Origin",
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
{
|
||||
"title": "A Pride Month Success: Over $8,400 Raised for The Trevor Project!",
|
||||
"summary": "A reflection on our Pride Month fundraiser campaign, which raised thousands for LGBTQ+ youth.",
|
||||
"summary": "Reflecting on our Pride Month fundraiser campaign for LGBTQ+ youth.",
|
||||
"thumbnail": "https://modrinth.com/news/article/pride-campaign-2025/thumbnail.webp",
|
||||
"date": "2025-07-01T18:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/pride-campaign-2025"
|
||||
@@ -114,14 +114,14 @@
|
||||
},
|
||||
{
|
||||
"title": "Creators can now make money on Modrinth!",
|
||||
"summary": "Yes, you read the title correctly: Modrinth's creator monetization program, also known as payouts, is now in an open beta phase. Read on for more information!",
|
||||
"summary": "Introducing the Creator Monetization Program allowing creators to earn revenue from their projects.",
|
||||
"thumbnail": "https://modrinth.com/news/article/creator-monetization/thumbnail.webp",
|
||||
"date": "2022-11-12T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/creator-monetization"
|
||||
},
|
||||
{
|
||||
"title": "Modrinth's Carbon Ads experiment",
|
||||
"summary": "As a step towards implementing author payouts, we're experimenting with a couple different ad providers to see which one works the best for us.",
|
||||
"summary": "Experimenting with a different ad providers to find one which one works for us.",
|
||||
"thumbnail": "https://modrinth.com/news/article/carbon-ads/thumbnail.webp",
|
||||
"date": "2022-09-08T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/carbon-ads"
|
||||
@@ -149,14 +149,14 @@
|
||||
},
|
||||
{
|
||||
"title": "This week in Modrinth development: Filters and Fixes",
|
||||
"summary": "After a great first week since Modrinth launched out of beta, we have continued to improve the user interface based on feedback.",
|
||||
"summary": "Continuing to improve the user interface after a great first week since Modrinth launched out of beta.",
|
||||
"thumbnail": "https://modrinth.com/news/article/knossos-v2.1.0/thumbnail.webp",
|
||||
"date": "2022-03-09T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/knossos-v2.1.0"
|
||||
},
|
||||
{
|
||||
"title": "Now showing on Modrinth: A new look!",
|
||||
"summary": "After months of relatively quiet development, Modrinth has released many new features and improvements, including a redesign. Read on to learn more!",
|
||||
"summary": "Releasing many new features and improvements, including a redesign!",
|
||||
"thumbnail": "https://modrinth.com/news/article/redesign/thumbnail.webp",
|
||||
"date": "2022-02-27T00:00:00.000Z",
|
||||
"link": "https://modrinth.com/news/article/redesign"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -123,6 +123,7 @@ public final class MinecraftLaunch {
|
||||
setAccessible0.setAccessible(true);
|
||||
setAccessible0.invoke(object, true);
|
||||
} catch (NoSuchMethodException e) {
|
||||
object.setAccessible(true);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
@@ -166,10 +166,18 @@ pub async fn test_jre(
|
||||
path: PathBuf,
|
||||
major_version: u32,
|
||||
) -> crate::Result<bool> {
|
||||
let Ok(jre) = jre::check_java_at_filepath(&path).await else {
|
||||
return Ok(false);
|
||||
let jre = match jre::check_java_at_filepath(&path).await {
|
||||
Ok(jre) => jre,
|
||||
Err(e) => {
|
||||
tracing::warn!("Invalid Java at {}: {e}", path.display());
|
||||
return Ok(false);
|
||||
}
|
||||
};
|
||||
let version = extract_java_version(&jre.version)?;
|
||||
tracing::info!(
|
||||
"Expected Java version {major_version}, and found {version} at {}",
|
||||
path.display()
|
||||
);
|
||||
Ok(version == major_version)
|
||||
}
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ pub static DEFAULT_SKINS: LazyLock<Vec<Skin>> = LazyLock::new(|| {
|
||||
Skin {
|
||||
texture_key: Arc::from("66206c8f51d13d2d31c54696a58a3e8bcd1e5e7db9888d331d0753129324e4f1"),
|
||||
name: Some(Arc::from("Party Alex")),
|
||||
variant: MinecraftSkinVariant::Classic,
|
||||
variant: MinecraftSkinVariant::Slim,
|
||||
cape_id: None,
|
||||
texture: Arc::from(Url::try_from(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAB3RJTUUH4QoJEjAE4f2wWAAAABd0RVh0U29mdHdhcmUAR0xEUE5HIHZlciAzLjRxhaThAAAACHRwTkdHTEQzAAAAAEqAKR8AAAAEZ0FNQQAAsY8L/GEFAAAABmJLR0QA/wD/AP+gvaeTAAAFFUlEQVR4nO1aXWhcRRS+XeMmClmbNVtTMdjUqlHrX5QSC0oMUnzxoVFBEAqSh4oRBEEEH0ooIvjggyDiSx4EkUgfLC1IEC0+iFKoQluFFNpGqLbotqtNGyVtynW/ac/s2ZN77+z9nb1NPviy586dmdxzZubMzJlxJicnnSBW3x92JX9/+34tH95xpyJPgwwi3VS/bRacFtBR6W5iZ7mk5bzDaIAwSi7W5hXzhJZ6QBAGnn1OsfTw6DIivd0R2wB5x6oBbH+Abax4A3jO8ybyOT8uc7EO4MA01/P0eO6mOz8UaO5ulVCeELasF22jpR7w6J5Oxb5X3nM6N9zlbHjtM/V8PcDTAFAUBKDo3JEfnLF3ruj3kJGGd+fOLDTlzx1o08K5NLvXXZj5QHHp6K/q2ZP1dzpf/VnWQ5sivjmS6badoCM/GoRCFy/+pxUDFz/9SKWBkCmd8nrVw5WFLHeLbWkAalHestwIXHkygJ/yZIB23i4vS7jv4yGXs7+/v4mmCu9+c8QF/Qyy6d3NgTTVzxtn/vPX9W9UA6SyEvzp8c16l8gJ3FguBtIEzEJgsacjkW/N9VL4hvKAc6U2p36jItcGSAQYdxjrNAYnzvTp8f/U93eotMqLWzUpH5WhMc+JMSlpcpTSSVJd0hETLx/8cNn/4P4hlA+4XLukDXLgy9u1XD2yztdwvIzEY4d+Ud2SODezP7ARsJgCaHmMaFJW0EOAO6DKQ39FrrBj09X1/SNf79cOywsv9A5oPn9PtzPSXdChNMAtn438DWGgDcBbNKjlvcA/trtyWhkBfOOWV50HD+zW77jSUsHyvWedoZPfaeI5CxR+3jjicB7a+LJzdHSXImSkndg+pCnzA2tqvbrCC9XGEOLDCUpT75BlgNqx4Oe0UNiy+xuHSFj87YQigeeRJHDlRrefbpJfGr5VvUfvkHm5DPCW73liNgEVg9G0moBCN5cqTRn+na8GVvDPqWtD51RX/c8lZ61Tqrd84z3vBX//OHg1/7W8jXJdOs/5w6VmuRF+SAWx1wFr+4tNBJaOlzzpld/EtOFpgGK9FxRFTwgCd2jrx45rJ7jtrVktU7rMjzQ8g+jyXM4CiawEuUPjM4jfmoLnp54BcMeXmRP0SuzpXa8YBtyZkbMD+JqCp/NewB0fl6WDTAWDg4N6qwsZHH5mTJGe5XtOuZ2V22lJ03ZYEstaLHs5Kc1rKUy/keMBkmHjAXmj9Q+wTWOGFd8DrnsDxI0H2FYgLlOJB3hh4qYZFwxVKAMkHg/4Yt2+ZUpC8Xa9UJVIPIADix0YAazu/ET9QvmFY384W//sMleQNZIeU+Q/tuzp06QYH9Jtj3nJVAwAR0rKk0xGsa1w6gYgI3jRtrKZGSBPtP4Btmn9A2yzMD097XLu++pb9+BsVRGyfC9pdQpLAtjTwxJ8jy/jAV55eHqeacwQdjMU934BnRHi/C+J838TjTHB8fHxNZwZdMpMYTTA1NSUy5nFRxGSOP83YfV+gNzrUxzAL5DJnzGGZHla9tLS16scl3lQk8Y/fuV9AHl5K1UfwHeDct9vigPww1Eu83JBdeB+QJaXLn2HAI/hAzxe0MplpijA3YDbxnamUrcffA3AWw/wa0F5dO51vB5E2+iYeGBbY2pbqjN6MCiXiD0LJHW/wBYSnQah0JO79jaxHZQMwopfB6RigLD3C2xitQekEQ+Icr/AGuLGA+LeL5Bn/Pz8X94JiHL+b+L/USxRwWwDOUgAAAAASUVORK5CYII="
|
||||
|
||||
@@ -633,6 +633,19 @@ pub async fn launch_minecraft(
|
||||
command.arg("--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED");
|
||||
}
|
||||
|
||||
// Patched by AstralRinth
|
||||
if credentials.access_token == "null" && credentials.refresh_token == "null" {
|
||||
if version_jar == "1.16.4" || version_jar == "1.16.5" {
|
||||
let invalid_url = "https://invalid.invalid";
|
||||
tracing::info!("✅ JVM args is patched by AstralRinth for MC {}", version_jar);
|
||||
command.arg("-Dminecraft.api.env=custom");
|
||||
command.arg(format!("-Dminecraft.api.auth.host={}", invalid_url));
|
||||
command.arg(format!("-Dminecraft.api.account.host={}", invalid_url));
|
||||
command.arg(format!("-Dminecraft.api.session.host={}", invalid_url));
|
||||
command.arg(format!("-Dminecraft.api.services.host={}", invalid_url));
|
||||
}
|
||||
}
|
||||
|
||||
command
|
||||
.arg("com.modrinth.theseus.MinecraftLaunch")
|
||||
.arg(version_info.main_class.clone())
|
||||
|
||||
@@ -3,6 +3,8 @@ use sqlx::sqlite::{
|
||||
SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions,
|
||||
};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use std::env;
|
||||
use tokio::time::Instant;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -17,8 +19,10 @@ pub(crate) async fn connect() -> crate::Result<Pool<Sqlite>> {
|
||||
crate::util::io::create_dir_all(&settings_dir).await?;
|
||||
}
|
||||
|
||||
let uri = format!("sqlite:{}", settings_dir.join("app.db").display());
|
||||
let db_path = settings_dir.join("app.db");
|
||||
let db_exists = db_path.exists();
|
||||
|
||||
let uri = format!("sqlite:{}", db_path.display());
|
||||
let conn_options = SqliteConnectOptions::from_str(&uri)?
|
||||
.busy_timeout(Duration::from_secs(30))
|
||||
.journal_mode(SqliteJournalMode::Wal)
|
||||
@@ -30,8 +34,16 @@ pub(crate) async fn connect() -> crate::Result<Pool<Sqlite>> {
|
||||
.connect_with(conn_options)
|
||||
.await?;
|
||||
|
||||
if db_exists {
|
||||
fix_modrinth_issued_migrations(&pool).await?;
|
||||
}
|
||||
|
||||
sqlx::migrate!().run(&pool).await?;
|
||||
|
||||
if !db_exists {
|
||||
fix_modrinth_issued_migrations(&pool).await?;
|
||||
}
|
||||
|
||||
if let Err(err) = stale_data_cleanup(&pool).await {
|
||||
tracing::warn!(
|
||||
"Failed to clean up stale data from state database: {err}"
|
||||
@@ -62,3 +74,73 @@ async fn stale_data_cleanup(pool: &Pool<Sqlite>) -> crate::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
/*
|
||||
// Patch by AstralRinth - 08.07.2025
|
||||
Problem files:
|
||||
/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
|
||||
*/
|
||||
async fn fix_modrinth_issued_migrations(
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> crate::Result<()> {
|
||||
let arch = env::consts::ARCH;
|
||||
let os = env::consts::OS;
|
||||
|
||||
tracing::info!("Running on OS: {}, ARCH: {}", os, arch);
|
||||
|
||||
if os == "windows" && arch == "x86_64" {
|
||||
tracing::warn!("🛑 Skipping migration checksum fix on Windows x86_64 (runtime-detected)");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let started = Instant::now();
|
||||
tracing::info!("Fixing modrinth issued migrations");
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE "_sqlx_migrations"
|
||||
SET checksum = X'e973512979feac07e415405291eefafc1ef0bd89454958ad66f5452c381db8679c20ffadab55194ecf6ba8ec4ca2db21'
|
||||
WHERE version = '20240711194701';
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
tracing::info!("⚙️ Fixed checksum for first migration");
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE "_sqlx_migrations"
|
||||
SET checksum = X'5b53534a7ffd74eebede234222be47e1d37bd0cc5fee4475212491b0c0379c16e3079e08eee0af959b1fa20835eeb206'
|
||||
WHERE version = '20240813205023';
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
tracing::info!("⚙️ Fixed checksum for second migration");
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE "_sqlx_migrations"
|
||||
SET checksum = X'c0de804f171b5530010edae087a6e75645c0e90177e28365f935c9fdd9a5c68e24850b8c1498e386a379d525d520bc57'
|
||||
WHERE version = '20240930001852';
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
tracing::info!("⚙️ Fixed checksum for third migration");
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE "_sqlx_migrations"
|
||||
SET checksum = X'c17542cb989a0466153e695bfa4717f8970feee185ca186a2caa1f2f6c5d4adb990ab97c26cacfbbe09c39ac81551704'
|
||||
WHERE version = '20241222013857';
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
tracing::info!("⚙️ Fixed checksum for fourth migration");
|
||||
let elapsed = started.elapsed();
|
||||
tracing::info!(
|
||||
"✅ Fixed all known Modrinth checksums for migrations in {:.2?}",
|
||||
elapsed
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ export const MicrosoftIcon = _MicrosoftIcon
|
||||
export const PirateShipIcon = _PirateShipIcon
|
||||
export const AstralRinthLogo = _AstralRinthLogo
|
||||
|
||||
export { default as CapeModel } from './models/cape.gltf?url'
|
||||
// Skin Models
|
||||
export { default as ClassicPlayerModel } from './models/classic-player.gltf?url'
|
||||
export { default as SlimPlayerModel } from './models/slim-player.gltf?url'
|
||||
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
{
|
||||
"asset": { "version": "2.0", "generator": "Blockbench 4.12.4 glTF exporter" },
|
||||
"scenes": [{ "nodes": [1], "name": "blockbench_export" }],
|
||||
"scene": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"rotation": [0, 0, 0.19509032201612825, 0.9807852804032304],
|
||||
"translation": [0.15625, 1, 0],
|
||||
"name": "Cape",
|
||||
"mesh": 0
|
||||
},
|
||||
{ "children": [0] }
|
||||
],
|
||||
"bufferViews": [
|
||||
{ "buffer": 0, "byteOffset": 0, "byteLength": 288, "target": 34962, "byteStride": 12 },
|
||||
{ "buffer": 0, "byteOffset": 288, "byteLength": 288, "target": 34962, "byteStride": 12 },
|
||||
{ "buffer": 0, "byteOffset": 576, "byteLength": 192, "target": 34962, "byteStride": 8 },
|
||||
{ "buffer": 0, "byteOffset": 768, "byteLength": 72, "target": 34963 }
|
||||
],
|
||||
"buffers": [
|
||||
{
|
||||
"byteLength": 840,
|
||||
"uri": "data:application/octet-stream;base64,AAAAPQAAAAAAAKA+AAAAPQAAAAAAAKC+AAAAPQAAgL8AAKA+AAAAPQAAgL8AAKC+AAAAvQAAAAAAAKC+AAAAvQAAAAAAAKA+AAAAvQAAgL8AAKC+AAAAvQAAgL8AAKA+AAAAvQAAAAAAAKC+AAAAPQAAAAAAAKC+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAvQAAgL8AAKA+AAAAPQAAgL8AAKA+AAAAvQAAgL8AAKC+AAAAPQAAgL8AAKC+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAvQAAgL8AAKA+AAAAPQAAgL8AAKA+AAAAPQAAAAAAAKC+AAAAvQAAAAAAAKC+AAAAPQAAgL8AAKC+AAAAvQAAgL8AAKC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPAAAgD0AADA+AACAPQAAgDwAAAg/AAAwPgAACD8AAEA+AACAPQAAsD4AAIA9AABAPgAACD8AALA+AAAIPwAAgDwAAAA9AACAPAAAgD0AADA+AAAAPQAAMD4AAIA9AAAwPgAAAD0AAKg+AAAAPQAAMD4AAAAAAACoPgAAAAAAAEA+AACAPQAAMD4AAIA9AABAPgAACD8AADA+AAAIPwAAAAAAAIA9AACAPAAAgD0AAAAAAAAIPwAAgDwAAAg/AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUA"
|
||||
}
|
||||
],
|
||||
"accessors": [
|
||||
{
|
||||
"bufferView": 0,
|
||||
"componentType": 5126,
|
||||
"count": 24,
|
||||
"max": [0.03125, 0, 0.3125],
|
||||
"min": [-0.03125, -1, -0.3125],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 1,
|
||||
"componentType": 5126,
|
||||
"count": 24,
|
||||
"max": [1, 1, 1],
|
||||
"min": [-1, -1, -1],
|
||||
"type": "VEC3"
|
||||
},
|
||||
{
|
||||
"bufferView": 2,
|
||||
"componentType": 5126,
|
||||
"count": 24,
|
||||
"max": [0.34375, 0.53125],
|
||||
"min": [0, 0],
|
||||
"type": "VEC2"
|
||||
},
|
||||
{
|
||||
"bufferView": 3,
|
||||
"componentType": 5123,
|
||||
"count": 36,
|
||||
"max": [23],
|
||||
"min": [0],
|
||||
"type": "SCALAR"
|
||||
}
|
||||
],
|
||||
"materials": [
|
||||
{
|
||||
"pbrMetallicRoughness": {
|
||||
"metallicFactor": 0,
|
||||
"roughnessFactor": 1,
|
||||
"baseColorTexture": { "index": 0 }
|
||||
},
|
||||
"alphaMode": "MASK",
|
||||
"alphaCutoff": 0.05,
|
||||
"doubleSided": true
|
||||
}
|
||||
],
|
||||
"textures": [{ "sampler": 0, "source": 0, "name": "cape.png" }],
|
||||
"samplers": [{ "magFilter": 9728, "minFilter": 9728, "wrapS": 33071, "wrapT": 33071 }],
|
||||
"images": [
|
||||
{
|
||||
"mimeType": "image/png",
|
||||
"uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAAAXNSR0IArs4c6QAABCRJREFUaEPtlktoE1EUhm/sIzFpYsBgHyn2oRZt0C5ESxWFYutKNyKIWBBR6kpBd3Ur2J0FRdCiuKmIIG4qLnzgQqTUXSlVqdoXTU0lljRpYtOHI/8dznBnOm1mphMwJWeTzJ0795zznf/cex2MMXam2S/ht38siJ8V1lgd5mOBARerc3rZnmK37rwvCyk2nE6wezMRh+4EncH9B1ql+fkU64rPsaBz5bqh4T7Daxn1Kc5zVNeEePJIci0Aq71bzenY6JChwAnA0OBHQ/OtJLnWNwqAy61R1tO3k891ueRKoDKwtqbv7MGbgCnfRgHQoqG9hyX4hc/yiho+/HNqVPFtdj2jwTpQgd/RKT7/0ZUku/o4qAJw50KYXbzr4e+3BioYzc3kwGzAUCLWFw2+UBiCb3bNTDHiPQeAP3CGNmg/6VcSBpDu3hhvDQpOCwABwrQKsRIsqYAChy/EQAXwlPiZ3a3igNPkXIyTjr8o5L7PPdzGf59c+sV/faeWeIIIAHPJ8M3kc7l1K09LKghWAIgqoPZ7djPFTlxb4D6yAgBOQfntrUVWVeRk44tplXJorOVGkVIJTBCTpw9ECFYBIEkyMfmsAXh3u1qi5MlxbbGX/x1ZSCjBAAwgfPr6h49R5UVavk0FXC2wju5p07s6iqEF0PtqSlEf+bKzDRwdgaDU7AkoySJ5Oo/D6ZRq/H0yyuJ/lxkSheG/FgCNm7kL0BoEAG32sqtY1YIHd2/mGzTMVgD3y2slqjgW9xY6ma9AThAARIMiBtOpjADwTWc0bEkB+FZsSTxTW0KBsGPXx0yvrUpEeHAAQIM7wBJLcu8DwHaP/H8i6VSND6SiSjBlRW4WWUypVIBbIgzjVgB0tpdKqLReS0KVPTMTfH0ra2cEgAmAAEd+l1z52LybqwBQYAQAyZPh6gtDW4jjuC4fHx8wXSm0JDbeI95SRYHiFZlUaWVtPQiKAugl5E8ASAUYiy8vc0C474uGasPE5PHc4g0wK/f4obom6UNimol7kTZwQLANwOuqBokqDEeQf4lfvvnNxZJcBTBAGZplWQcQ3tcgwY9oWlXinRW4ugoAgNAWWe6ocn1QvgyRTUb4RZFVljlY/3hSBYCqb6cCZo8ekuATVRZPI/gQW8FWAI1VHganVgDQUajdA6y2gAgAcZFBjTBSpG0AcAqc3VVmCMDTbxGWZvIRCaMNkJ7pFMCzVQD4liCQ8kRFUlvaBkCvL+wYw2ZmNUgCgLajFhRPJlv3ADuS1VtjPQCk823S574ffN/x1dQqy8dHR5SN2Spcbaymz2mjwNYDAD4IQn3TDpVLQIAqNjwAANRrAdoI/3sAOF7Xe1khCNQGVH1bL0JGJb1R52VtD8gVYHkAuVKpbMWZV0C2yObKunkF5EqlshVnXgHZIpsr6/4DlbxcPydnT74AAAAASUVORK5CYII="
|
||||
}
|
||||
],
|
||||
"meshes": [
|
||||
{
|
||||
"primitives": [
|
||||
{
|
||||
"mode": 4,
|
||||
"attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 },
|
||||
"indices": 3,
|
||||
"material": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
packages/assets/models/classic-player.fbx
Normal file
BIN
packages/assets/models/classic-player.fbx
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
packages/assets/models/slim-player.fbx
Normal file
BIN
packages/assets/models/slim-player.fbx
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@
|
||||
title: A New Chapter for Modrinth Servers
|
||||
summary: Modrinth Servers is now fully operated in-house by the Modrinth Team.
|
||||
date: 2025-03-13T00:00:00+00:00
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG']
|
||||
---
|
||||
|
||||
Over the few months, Modrinth has seen incredible interest towards our Servers product, and with significant growth, our vision for what Modrinth Servers can be has evolved alongside it. To continue striving towards our goal of providing the best place to get your own Minecraft multiplayer server, we’ve made the decision to bring our server hosting fully in-house.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: Accelerating Modrinth's Development
|
||||
summary: Our fundraiser and the future of Modrinth!
|
||||
date: 2023-02-01T12:00:00-08:00
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG', '6plzAzU4']
|
||||
---
|
||||
|
||||
**Update: On [April 4, 2024](/news/article/capital-return) we announced that we had returned the remaining $800k in investor capital back to our investors to take a different path. [Read that announcement here](/news/article/capital-return). This article remains here for archival purposes.**
|
||||
|
||||
@@ -4,6 +4,7 @@ short_title: Becoming Sustainable
|
||||
summary: Announcing an update to our monetization program, creator split, and more!
|
||||
short_summary: Announcing 5x creator revenue and updates to the monetization program.
|
||||
date: 2024-09-13T12:00:00-08:00
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG']
|
||||
---
|
||||
|
||||
Just over 3 weeks ago, we [launched](/news/article/introducing-modrinth-refreshed-site-look-new-advertising-system) our new ads powered by [Aditude](https://www.aditude.com/). These ads have allowed us to improve creator revenue drastically and become sustainable. Read on for more info!
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: A Sustainable Path Forward for Modrinth
|
||||
summary: Our capital return and what’s next.
|
||||
date: 2024-04-04T12:00:00-08:00
|
||||
authors: ['MpxzqsyW']
|
||||
---
|
||||
|
||||
Over three years ago, I started Modrinth: a new Minecraft modding platform built on community principles, a fully open-source codebase, and a focus on creators.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
title: Modrinth's Carbon Ads experiment
|
||||
summary: "As a step towards implementing author payouts, we're experimenting with a couple different ad providers to see which one works the best for us."
|
||||
summary: 'Experimenting with a different ad providers to find one which one works for us.'
|
||||
date: 2022-09-08
|
||||
authors: ['6plzAzU4']
|
||||
---
|
||||
|
||||
**Update 10/24:** A month and a half ago we began testing Carbon Ads on Modrinth, and in the end, using Carbon did not work out. After disabling ads with tracking in them, the revenue was about equal to or worse than what we were generating previously with EthicalAds. Effective today, we are switching our ads provider back to EthicalAds for the time being.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
---
|
||||
title: Creators can now make money on Modrinth!
|
||||
summary: "Yes, you read the title correctly: Modrinth's creator monetization program, also known as payouts, is now in an open beta phase. Read on for more information!"
|
||||
summary: 'Introducing the Creator Monetization Program allowing creators to earn revenue from their projects.'
|
||||
date: 2022-11-12
|
||||
authors: ['6plzAzU4']
|
||||
---
|
||||
|
||||
Yes, you read the title correctly: Modrinth's Creator Monetization Program, also known as payouts, is now in an open beta phase. All of the money that project owners have earned since August 1st is available to claim **right now**!
|
||||
|
||||
@@ -4,6 +4,7 @@ short_title: The Creator Update
|
||||
summary: December may be over, but we’re not done giving gifts.
|
||||
short_summary: Adding analytics, orgs, collections, and more!
|
||||
date: 2024-01-06T12:00:00-08:00
|
||||
authors: ['6plzAzU4']
|
||||
---
|
||||
|
||||
December may be over, but that doesn’t mean we’re done giving gifts here at Modrinth. Over the past few months, we’ve been cooking up a whole bunch of new features for everyone to enjoy. Now seems like as good of a time as ever to bring you our Creator Update! Buckle up, because this is a big one.
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: Creator Updates, July 2025
|
||||
summary: Addressing recent growth and growing pains that have been affecting creators.
|
||||
date: 2025-07-01T21:20:00-07:00
|
||||
authors: ['MpxzqsyW']
|
||||
---
|
||||
|
||||
Hey all,
|
||||
|
||||
@@ -4,6 +4,7 @@ short_title: Modrinth+ and New Ads
|
||||
summary: Learn about this major update to Modrinth.
|
||||
short_summary: Introducing a new ad system, a subscription to remove ads, and a redesign of the website!
|
||||
date: 2024-08-21T12:00:00-08:00
|
||||
authors: ['MpxzqsyW', 'Dc7EYhxG']
|
||||
---
|
||||
|
||||
We’ve got a big launch with tons of new stuff today and some important updates about Modrinth. Read on, because we have a lot to cover!
|
||||
|
||||
@@ -3,6 +3,7 @@ title: Correcting Inflated Download Counts due to Rate Limiting Issue
|
||||
short_title: Correcting Inflated Download Counts
|
||||
summary: A rate limiting issue caused inflated download counts in certain countries.
|
||||
date: 2023-11-10T12:00:00-08:00
|
||||
authors: ['6plzAzU4', 'MpxzqsyW']
|
||||
---
|
||||
|
||||
While working on the upcoming analytics update for Modrinth, our team found an issue leading to higher download counts from specific countries. This was caused by an oversight with regards to rate limiting, or in other words, certain files being downloaded over and over again. **Importantly, this did not affect creator payouts**; only our analytics. Approximately 15.4% of Modrinth downloads were found to be over-counted. These duplicates have been identified and are being removed from project download counts and analytics. Read on to learn about the cause of this error and how we fixed it.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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 doesn’t mean we haven’t 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. Let’s get right into it!
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
title: 'Skins — Now in Modrinth App!'
|
||||
summary: 'Customize your look, save your favorite skins, and swap them out in a flash, all within Modrinth App.'
|
||||
date: 2025-07-06T16:45:00-07:00
|
||||
authors: [bOHH0P9Z, Dc7EYhxG]
|
||||
---
|
||||
|
||||
We're thrilled to roll out Modrinth App **v0.10** with a beta release of one of our most highly requested features, the **Skins page**. The Skins page allows you to manage all of your Minecraft skins directly within Modrinth App. You can see all your saved custom skins and the default Minecraft skins in one convenient place.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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])},`)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -5,5 +5,6 @@ export const article = {
|
||||
summary: 'Our capital return and what’s next.',
|
||||
date: '2024-04-04T20:00:00.000Z',
|
||||
slug: 'capital-return',
|
||||
authors: ['MpxzqsyW'],
|
||||
thumbnail: false,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export const article = {
|
||||
summary: 'December may be over, but we’re 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!',
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
export const article = {
|
||||
html: () => import(`./pride_campaign_2025.content`).then((m) => m.html),
|
||||
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.',
|
||||
date: '2025-07-01T18:00:00.000Z',
|
||||
slug: 'pride-campaign-2025',
|
||||
authors: ['6plzAzU4', 'bOHH0P9Z', '2cqK8Q5p', 'vNcGR3Fd'],
|
||||
thumbnail: true,
|
||||
short_title: 'Pride Month Fundraiser 2025',
|
||||
short_summary: 'A reflection on our Pride Month fundraiser campaign.',
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
export const article = {
|
||||
html: () => import(`./redesign.content`).then((m) => m.html),
|
||||
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-27T00:00:00.000Z',
|
||||
slug: 'redesign',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@ export const article = {
|
||||
'Customize your look, save your favorite skins, and swap them out in a flash, all within Modrinth App.',
|
||||
date: '2025-07-06T23:45:00.000Z',
|
||||
slug: 'skins-now-in-modrinth-app',
|
||||
authors: ['bOHH0P9Z', 'Dc7EYhxG'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ export const article = {
|
||||
summary: "Marking two years of Modrinth and discussing our New Year's Resolutions for 2023.",
|
||||
date: '2023-01-07T00:00:00.000Z',
|
||||
slug: 'two-years-of-modrinth',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ export const article = {
|
||||
summary: 'The history of Modrinth as we know it from December 2020 to December 2022.',
|
||||
date: '2023-01-07T00:00:00.000Z',
|
||||
slug: 'two-years-of-modrinth-history',
|
||||
authors: ['6plzAzU4'],
|
||||
thumbnail: false,
|
||||
}
|
||||
|
||||
@@ -6,5 +6,6 @@ export const article = {
|
||||
"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-27T00:00:00.000Z',
|
||||
slug: 'whats-modrinth',
|
||||
authors: ['aNd6VJql'],
|
||||
thumbnail: false,
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@ export const article = {
|
||||
summary: 'Threat Analysis and Plan of Action',
|
||||
date: '2024-05-07T20:00:00.000Z',
|
||||
slug: 'windows-borderless-malware-disclosure',
|
||||
authors: ['Dc7EYhxG', 'MpxzqsyW'],
|
||||
thumbnail: true,
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"@modrinth/utils": "workspace:*",
|
||||
"@tresjs/cientos": "^4.3.0",
|
||||
"@tresjs/core": "^4.3.4",
|
||||
"@tresjs/post-processing": "^2.4.0",
|
||||
"@types/markdown-it": "^14.1.1",
|
||||
"@types/three": "^0.172.0",
|
||||
"@vintl/how-ago": "^3.0.1",
|
||||
@@ -41,6 +42,7 @@
|
||||
"floating-vue": "^5.2.2",
|
||||
"highlight.js": "^11.9.0",
|
||||
"markdown-it": "^13.0.2",
|
||||
"postprocessing": "^6.37.6",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"three": "^0.172.0",
|
||||
"vue-multiselect": "3.0.0",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<TresCanvas
|
||||
shadows
|
||||
alpha
|
||||
:antialias="antialias"
|
||||
:antialias="true"
|
||||
:renderer-options="{
|
||||
outputColorSpace: THREE.SRGBColorSpace,
|
||||
toneMapping: THREE.NoToneMapping,
|
||||
@@ -46,36 +46,39 @@
|
||||
<primitive v-if="scene" :object="scene" />
|
||||
</Group>
|
||||
|
||||
<TresMesh
|
||||
<!-- <TresMesh
|
||||
:position="[0, -0.095 * scale, 2]"
|
||||
:rotation="[-Math.PI / 2, 0, 0]"
|
||||
:scale="[0.5 * 0.75 * scale, 0.5 * 0.75 * scale, 0.5 * 0.75 * scale]"
|
||||
:scale="[0.4 * 0.75 * scale, 0.4 * 0.75 * scale, 0.4 * 0.75 * scale]"
|
||||
>
|
||||
<TresCircleGeometry :args="[1, 128]" />
|
||||
<TresMeshBasicMaterial
|
||||
color="#000000"
|
||||
:opacity="0.2"
|
||||
:opacity="0.5"
|
||||
transparent
|
||||
:depth-write="false"
|
||||
/>
|
||||
</TresMesh>
|
||||
|
||||
<TresMesh
|
||||
:position="[0, -0.1 * scale, 2]"
|
||||
:rotation="[-Math.PI / 2, 0, 0]"
|
||||
:scale="[0.75 * scale, 0.75 * scale, 0.75 * scale]"
|
||||
>
|
||||
<TresCircleGeometry :args="[1, 128]" />
|
||||
<TresMeshBasicMaterial
|
||||
:map="radialTexture"
|
||||
transparent
|
||||
:depth-write="false"
|
||||
:blending="THREE.AdditiveBlending"
|
||||
/>
|
||||
</TresMesh>
|
||||
</TresMesh> -->
|
||||
</Group>
|
||||
</Suspense>
|
||||
|
||||
<Suspense>
|
||||
<EffectComposerPmndrs>
|
||||
<FXAAPmndrs />
|
||||
</EffectComposerPmndrs>
|
||||
</Suspense>
|
||||
|
||||
<Suspense>
|
||||
<TresMesh
|
||||
:position="[0, -0.1 * scale, 2]"
|
||||
:rotation="[-Math.PI / 2, 0, 0]"
|
||||
:scale="[0.75 * scale, 0.75 * scale, 0.75 * scale]"
|
||||
>
|
||||
<TresCircleGeometry :args="[1, 128]" />
|
||||
<TresShaderMaterial v-bind="radialSpotlightShader" />
|
||||
</TresMesh>
|
||||
</Suspense>
|
||||
|
||||
<TresPerspectiveCamera
|
||||
:make-default.camel="true"
|
||||
:fov="fov"
|
||||
@@ -101,6 +104,7 @@
|
||||
import * as THREE from 'three'
|
||||
import { useGLTF } from '@tresjs/cientos'
|
||||
import { useTexture, TresCanvas, useRenderLoop } from '@tresjs/core'
|
||||
import { EffectComposerPmndrs, FXAAPmndrs } from '@tresjs/post-processing'
|
||||
import {
|
||||
shallowRef,
|
||||
ref,
|
||||
@@ -115,13 +119,11 @@ import {
|
||||
import {
|
||||
applyTexture,
|
||||
applyCapeTexture,
|
||||
attachCapeToBody,
|
||||
findBodyNode,
|
||||
createTransparentTexture,
|
||||
loadTexture as loadSkinTexture,
|
||||
} from '@modrinth/utils'
|
||||
import { useDynamicFontSize } from '../../composables'
|
||||
import { CapeModel, ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
||||
import { ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
|
||||
|
||||
interface AnimationConfig {
|
||||
baseAnimation: string
|
||||
@@ -136,7 +138,6 @@ const props = withDefaults(
|
||||
capeSrc?: string
|
||||
variant?: 'SLIM' | 'CLASSIC' | 'UNKNOWN'
|
||||
nametag?: string
|
||||
antialias?: boolean
|
||||
scale?: number
|
||||
fov?: number
|
||||
initialRotation?: number
|
||||
@@ -144,7 +145,6 @@ const props = withDefaults(
|
||||
}>(),
|
||||
{
|
||||
variant: 'CLASSIC',
|
||||
antialias: false,
|
||||
scale: 1,
|
||||
fov: 40,
|
||||
capeSrc: undefined,
|
||||
@@ -177,9 +177,6 @@ const selectedModelSrc = computed(() =>
|
||||
)
|
||||
|
||||
const scene = shallowRef<THREE.Object3D | null>(null)
|
||||
const capeScene = shallowRef<THREE.Object3D | null>(null)
|
||||
const bodyNode = shallowRef<THREE.Object3D | null>(null)
|
||||
const capeAttached = ref(false)
|
||||
const lastCapeSrc = ref<string | undefined>(undefined)
|
||||
const texture = shallowRef<THREE.Texture | null>(null)
|
||||
const capeTexture = shallowRef<THREE.Texture | null>(null)
|
||||
@@ -196,6 +193,54 @@ const currentAnimation = ref<string>('')
|
||||
const randomAnimationTimer = ref<number | null>(null)
|
||||
const lastRandomAnimation = ref<string>('')
|
||||
|
||||
const radialSpotlightShader = computed(() => ({
|
||||
uniforms: {
|
||||
innerColor: { value: new THREE.Color(0x000000) },
|
||||
outerColor: { value: new THREE.Color(0xffffff) },
|
||||
innerOpacity: { value: 0.3 },
|
||||
outerOpacity: { value: 0.0 },
|
||||
falloffPower: { value: 1.2 },
|
||||
shadowRadius: { value: 7 },
|
||||
},
|
||||
vertexShader: `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform vec3 innerColor;
|
||||
uniform vec3 outerColor;
|
||||
uniform float innerOpacity;
|
||||
uniform float outerOpacity;
|
||||
uniform float falloffPower;
|
||||
uniform float shadowRadius;
|
||||
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vec2 center = vec2(0.5, 0.5);
|
||||
float dist = distance(vUv, center) * 2.0;
|
||||
|
||||
// Create shadow in the center
|
||||
float shadowFalloff = 1.0 - smoothstep(0.0, shadowRadius, dist);
|
||||
|
||||
// Create overall spotlight falloff
|
||||
float spotlightFalloff = 1.0 - smoothstep(0.0, 1.0, pow(dist, falloffPower));
|
||||
|
||||
// Combine both effects
|
||||
vec3 color = mix(outerColor, innerColor, shadowFalloff);
|
||||
float opacity = mix(outerOpacity, innerOpacity * shadowFalloff, spotlightFalloff);
|
||||
|
||||
gl_FragColor = vec4(color, opacity);
|
||||
}
|
||||
`,
|
||||
transparent: true,
|
||||
depthWrite: false,
|
||||
depthTest: false,
|
||||
}))
|
||||
|
||||
const { baseAnimation, randomAnimations } = toRefs(props.animationConfig)
|
||||
|
||||
function initializeAnimations(loadedScene: THREE.Object3D, clips: THREE.AnimationClip[]) {
|
||||
@@ -400,11 +445,9 @@ async function loadModel(src: string) {
|
||||
|
||||
if (texture.value) {
|
||||
applyTexture(scene.value, texture.value)
|
||||
texture.value.needsUpdate = true
|
||||
}
|
||||
|
||||
bodyNode.value = findBodyNode(loadedScene)
|
||||
capeAttached.value = false
|
||||
applyCapeTexture(scene.value, capeTexture.value, transparentTexture)
|
||||
|
||||
if (animations && animations.length > 0) {
|
||||
initializeAnimations(loadedScene, animations)
|
||||
@@ -418,22 +461,6 @@ async function loadModel(src: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCape() {
|
||||
try {
|
||||
const { scene: loadedCape } = await useGLTF(CapeModel)
|
||||
capeScene.value = markRaw(loadedCape)
|
||||
|
||||
applyCapeTexture(capeScene.value, capeTexture.value, transparentTexture)
|
||||
|
||||
if (bodyNode.value && !capeAttached.value) {
|
||||
attachCapeToBodyWrapper()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load cape:', error)
|
||||
capeScene.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAndApplyTexture(src: string) {
|
||||
if (!src) return null
|
||||
|
||||
@@ -465,25 +492,9 @@ async function loadAndApplyCapeTexture(src: string | undefined) {
|
||||
capeTexture.value = null
|
||||
}
|
||||
|
||||
if (capeScene.value) {
|
||||
applyCapeTexture(capeScene.value, capeTexture.value, transparentTexture)
|
||||
if (scene.value) {
|
||||
applyCapeTexture(scene.value, capeTexture.value, transparentTexture)
|
||||
}
|
||||
|
||||
if (capeScene.value && bodyNode.value) {
|
||||
if (!src && capeAttached.value && capeScene.value.parent) {
|
||||
capeScene.value.parent.remove(capeScene.value)
|
||||
capeAttached.value = false
|
||||
} else if (src && !capeAttached.value) {
|
||||
attachCapeToBodyWrapper()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function attachCapeToBodyWrapper() {
|
||||
if (!bodyNode.value || !capeScene.value || capeAttached.value) return
|
||||
|
||||
attachCapeToBody(bodyNode.value, capeScene.value)
|
||||
capeAttached.value = true
|
||||
}
|
||||
|
||||
const centre = ref<[number, number, number]>([0, 1, 0])
|
||||
@@ -539,39 +550,6 @@ function onCanvasClick() {
|
||||
hasDragged.value = false
|
||||
}
|
||||
|
||||
const radialTexture = createRadialTexture(512)
|
||||
radialTexture.minFilter = THREE.LinearFilter
|
||||
radialTexture.magFilter = THREE.LinearFilter
|
||||
radialTexture.wrapS = radialTexture.wrapT = THREE.ClampToEdgeWrapping
|
||||
|
||||
function createRadialTexture(size: number): THREE.CanvasTexture {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = canvas.height = size
|
||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
|
||||
const grad = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2)
|
||||
grad.addColorStop(0, 'rgba(119,119,119,0.1)')
|
||||
grad.addColorStop(0.9, 'rgba(255,255,255,0)')
|
||||
ctx.fillStyle = grad
|
||||
ctx.fillRect(0, 0, size, size)
|
||||
return new THREE.CanvasTexture(canvas)
|
||||
}
|
||||
|
||||
watch(
|
||||
[bodyNode, capeScene, isModelLoaded],
|
||||
([newBodyNode, newCapeScene, modelLoaded]) => {
|
||||
if (newBodyNode && newCapeScene && modelLoaded && !capeAttached.value) {
|
||||
attachCapeToBodyWrapper()
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(capeScene, (newCapeScene) => {
|
||||
if (newCapeScene && bodyNode.value && isModelLoaded.value && !capeAttached.value) {
|
||||
attachCapeToBodyWrapper()
|
||||
}
|
||||
})
|
||||
|
||||
watch(selectedModelSrc, (src) => loadModel(src))
|
||||
watch(
|
||||
() => props.textureSrc,
|
||||
@@ -587,7 +565,6 @@ watch(
|
||||
watch(
|
||||
() => props.capeSrc,
|
||||
async (newCapeSrc) => {
|
||||
await loadCape()
|
||||
await loadAndApplyCapeTexture(newCapeSrc)
|
||||
},
|
||||
)
|
||||
@@ -619,8 +596,6 @@ onBeforeMount(async () => {
|
||||
if (props.capeSrc) {
|
||||
await loadAndApplyCapeTexture(props.capeSrc)
|
||||
}
|
||||
|
||||
await loadCape()
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize skin preview:', error)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,30 @@ export type VersionEntry = {
|
||||
}
|
||||
|
||||
const VERSIONS: VersionEntry[] = [
|
||||
{
|
||||
date: `2025-07-08T14:00:00-07:00`,
|
||||
product: 'web',
|
||||
body: `### Improvements
|
||||
- Fixed Modrinth Servers showing as out of stock when navigating to the page directly.`,
|
||||
},
|
||||
{
|
||||
date: `2025-07-08T11:10:00-07:00`,
|
||||
product: 'servers',
|
||||
body: `### Improvements
|
||||
- Reapplied error handling improvements, with more improvements.`,
|
||||
},
|
||||
{
|
||||
date: `2025-07-07T22:20:00-07:00`,
|
||||
product: 'servers',
|
||||
body: `### Improvements
|
||||
- Fixed issue with Servers panel failing to load.`,
|
||||
},
|
||||
{
|
||||
date: `2025-07-07T17:45:00-07:00`,
|
||||
product: 'servers',
|
||||
body: `### Improvements
|
||||
- Reverted error handling improvements.`,
|
||||
},
|
||||
{
|
||||
date: `2025-07-07T01:10:00-07:00`,
|
||||
product: 'app',
|
||||
@@ -73,7 +97,8 @@ const VERSIONS: VersionEntry[] = [
|
||||
date: `2025-07-04T12:00:00-07:00`,
|
||||
product: 'web',
|
||||
body: `### Changed
|
||||
- Changed fallback ad placeholder from promoting Modrinth+ to Modrinth Servers.`,
|
||||
- Changed fallback ad placeholder from promoting Modrinth+ to Modrinth Servers.
|
||||
- Fixed news section rendering incorrectly in light mode on landing page and Modrinth App page.`,
|
||||
},
|
||||
{
|
||||
date: `2025-06-30T19:15:00-07:00`,
|
||||
|
||||
@@ -59,25 +59,23 @@ export function applyTexture(model: THREE.Object3D, texture: THREE.Texture): voi
|
||||
model.traverse((child) => {
|
||||
if ((child as THREE.Mesh).isMesh) {
|
||||
const mesh = child as THREE.Mesh
|
||||
|
||||
// Skip cape meshes
|
||||
if (mesh.name === 'Cape') return
|
||||
|
||||
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
|
||||
|
||||
materials.forEach((mat: THREE.Material) => {
|
||||
if (mat instanceof THREE.MeshStandardMaterial) {
|
||||
mat.map = texture
|
||||
mat.metalness = 0
|
||||
mat.color.set(0xffffff)
|
||||
mat.toneMapped = false
|
||||
mat.flatShading = true
|
||||
mat.roughness = 1
|
||||
mat.needsUpdate = true
|
||||
mat.depthTest = true
|
||||
mat.side = THREE.DoubleSide
|
||||
mat.alphaTest = 0.1
|
||||
mat.depthWrite = true
|
||||
if (mat.name !== 'cape') {
|
||||
mat.map = texture
|
||||
mat.metalness = 0
|
||||
mat.color.set(0xffffff)
|
||||
mat.toneMapped = false
|
||||
mat.flatShading = true
|
||||
mat.roughness = 1
|
||||
mat.needsUpdate = true
|
||||
mat.depthTest = true
|
||||
mat.side = THREE.DoubleSide
|
||||
mat.alphaTest = 0.1
|
||||
mat.depthWrite = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -96,41 +94,27 @@ export function applyCapeTexture(
|
||||
|
||||
materials.forEach((mat: THREE.Material) => {
|
||||
if (mat instanceof THREE.MeshStandardMaterial) {
|
||||
mat.map = texture || transparentTexture || null
|
||||
mat.transparent = transparentTexture ? true : false
|
||||
mat.metalness = 0
|
||||
mat.color.set(0xffffff)
|
||||
mat.toneMapped = false
|
||||
mat.flatShading = true
|
||||
mat.roughness = 1
|
||||
mat.needsUpdate = true
|
||||
mat.depthTest = true
|
||||
mat.depthWrite = true
|
||||
mat.side = THREE.DoubleSide
|
||||
mat.alphaTest = 0.1
|
||||
if (mat.name === 'cape') {
|
||||
mat.map = texture || transparentTexture || null
|
||||
mat.transparent = !texture || transparentTexture ? true : false
|
||||
mat.metalness = 0
|
||||
mat.color.set(0xffffff)
|
||||
mat.toneMapped = false
|
||||
mat.flatShading = true
|
||||
mat.roughness = 1
|
||||
mat.needsUpdate = true
|
||||
mat.depthTest = true
|
||||
mat.depthWrite = true
|
||||
mat.side = THREE.DoubleSide
|
||||
mat.alphaTest = 0.1
|
||||
mat.visible = !!texture
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function attachCapeToBody(
|
||||
bodyNode: THREE.Object3D,
|
||||
capeModel: THREE.Object3D,
|
||||
position = { x: 0, y: -1, z: 0.01 },
|
||||
rotation = { x: 0, y: Math.PI / 2, z: 0 },
|
||||
): void {
|
||||
if (!bodyNode || !capeModel) return
|
||||
|
||||
if (capeModel.parent) {
|
||||
capeModel.parent.remove(capeModel)
|
||||
}
|
||||
|
||||
capeModel.position.set(position.x, position.y, position.z)
|
||||
capeModel.rotation.set(rotation.x, rotation.y, rotation.z)
|
||||
bodyNode.add(capeModel)
|
||||
}
|
||||
|
||||
export function findBodyNode(model: THREE.Object3D): THREE.Object3D | null {
|
||||
let bodyNode: THREE.Object3D | null = null
|
||||
|
||||
@@ -162,39 +146,25 @@ export function createTransparentTexture(): THREE.Texture {
|
||||
export async function setupSkinModel(
|
||||
modelUrl: string,
|
||||
textureUrl: string,
|
||||
capeModelUrl?: string,
|
||||
capeTextureUrl?: string,
|
||||
config: SkinRendererConfig = {},
|
||||
): Promise<{
|
||||
model: THREE.Object3D
|
||||
bodyNode: THREE.Object3D | null
|
||||
capeModel: THREE.Object3D | null
|
||||
}> {
|
||||
// Load model and texture in parallel
|
||||
const [gltf, texture] = await Promise.all([loadModel(modelUrl), loadTexture(textureUrl, config)])
|
||||
|
||||
const model = gltf.scene.clone()
|
||||
applyTexture(model, texture)
|
||||
|
||||
const bodyNode = findBodyNode(model)
|
||||
let capeModel: THREE.Object3D | null = null
|
||||
|
||||
// Load cape if provided
|
||||
if (capeModelUrl && capeTextureUrl) {
|
||||
const [capeGltf, capeTexture] = await Promise.all([
|
||||
loadModel(capeModelUrl),
|
||||
loadTexture(capeTextureUrl, config),
|
||||
])
|
||||
|
||||
capeModel = capeGltf.scene.clone()
|
||||
applyCapeTexture(capeModel, capeTexture)
|
||||
|
||||
if (bodyNode && capeModel) {
|
||||
attachCapeToBody(bodyNode, capeModel)
|
||||
}
|
||||
if (capeTextureUrl) {
|
||||
const capeTexture = await loadTexture(capeTextureUrl, config)
|
||||
applyCapeTexture(model, capeTexture)
|
||||
}
|
||||
|
||||
return { model, bodyNode, capeModel }
|
||||
const bodyNode = findBodyNode(model)
|
||||
|
||||
return { model, bodyNode }
|
||||
}
|
||||
|
||||
export function disposeCaches(): void {
|
||||
|
||||
103
patches/db_migrations_fix.patch
Normal file
103
patches/db_migrations_fix.patch
Normal file
@@ -0,0 +1,103 @@
|
||||
diff --git a/packages/app-lib/src/state/db.rs b/packages/app-lib/src/state/db.rs
|
||||
index 14c53d81..607a345f 100644
|
||||
--- a/packages/app-lib/src/state/db.rs
|
||||
+++ b/packages/app-lib/src/state/db.rs
|
||||
@@ -3,6 +3,7 @@ use sqlx::sqlite::{
|
||||
SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions,
|
||||
};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
+use tokio::time::Instant;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -17,8 +18,10 @@ pub(crate) async fn connect() -> crate::Result<Pool<Sqlite>> {
|
||||
crate::util::io::create_dir_all(&settings_dir).await?;
|
||||
}
|
||||
|
||||
- let uri = format!("sqlite:{}", settings_dir.join("app.db").display());
|
||||
+ let db_path = settings_dir.join("app.db");
|
||||
+ let db_exists = db_path.exists();
|
||||
|
||||
+ let uri = format!("sqlite:{}", db_path.display());
|
||||
let conn_options = SqliteConnectOptions::from_str(&uri)?
|
||||
.busy_timeout(Duration::from_secs(30))
|
||||
.journal_mode(SqliteJournalMode::Wal)
|
||||
@@ -30,10 +33,16 @@ pub(crate) async fn connect() -> crate::Result<Pool<Sqlite>> {
|
||||
.connect_with(conn_options)
|
||||
.await?;
|
||||
|
||||
- fix_migration_20240711194701(&pool).await?; // Patch by AstralRinth - 08.07.2025
|
||||
+ if db_exists {
|
||||
+ fix_modrinth_issued_migrations(&pool).await?;
|
||||
+ }
|
||||
|
||||
sqlx::migrate!().run(&pool).await?;
|
||||
|
||||
+ if !db_exists {
|
||||
+ fix_modrinth_issued_migrations(&pool).await?;
|
||||
+ }
|
||||
+
|
||||
if let Err(err) = stale_data_cleanup(&pool).await {
|
||||
tracing::warn!(
|
||||
"Failed to clean up stale data from state database: {err}"
|
||||
@@ -66,8 +75,17 @@ async fn stale_data_cleanup(pool: &Pool<Sqlite>) -> crate::Result<()> {
|
||||
}
|
||||
/*
|
||||
// Patch by AstralRinth - 08.07.2025
|
||||
+Problem files:
|
||||
+/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
|
||||
*/
|
||||
-async fn fix_migration_20240711194701(pool: &Pool<Sqlite>) -> crate::Result<()> {
|
||||
+async fn fix_modrinth_issued_migrations(
|
||||
+ pool: &Pool<Sqlite>,
|
||||
+) -> crate::Result<()> {
|
||||
+ let started = Instant::now();
|
||||
+ tracing::info!("Fixing modrinth issued migrations");
|
||||
sqlx::query(
|
||||
r#"
|
||||
UPDATE "_sqlx_migrations"
|
||||
@@ -77,5 +95,41 @@ async fn fix_migration_20240711194701(pool: &Pool<Sqlite>) -> crate::Result<()>
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
+ tracing::info!("⚙️ Fixed first migration");
|
||||
+ sqlx::query(
|
||||
+ r#"
|
||||
+ UPDATE "_sqlx_migrations"
|
||||
+ SET checksum = X'5b53534a7ffd74eebede234222be47e1d37bd0cc5fee4475212491b0c0379c16e3079e08eee0af959b1fa20835eeb206'
|
||||
+ WHERE version = '20240813205023';
|
||||
+ "#,
|
||||
+ )
|
||||
+ .execute(pool)
|
||||
+ .await?;
|
||||
+ tracing::info!("⚙️ Fixed second migration");
|
||||
+ sqlx::query(
|
||||
+ r#"
|
||||
+ UPDATE "_sqlx_migrations"
|
||||
+ SET checksum = X'c0de804f171b5530010edae087a6e75645c0e90177e28365f935c9fdd9a5c68e24850b8c1498e386a379d525d520bc57'
|
||||
+ WHERE version = '20240930001852';
|
||||
+ "#,
|
||||
+ )
|
||||
+ .execute(pool)
|
||||
+ .await?;
|
||||
+ tracing::info!("⚙️ Fixed third migration");
|
||||
+ sqlx::query(
|
||||
+ r#"
|
||||
+ UPDATE "_sqlx_migrations"
|
||||
+ SET checksum = X'c17542cb989a0466153e695bfa4717f8970feee185ca186a2caa1f2f6c5d4adb990ab97c26cacfbbe09c39ac81551704'
|
||||
+ WHERE version = '20241222013857';
|
||||
+ "#,
|
||||
+ )
|
||||
+ .execute(pool)
|
||||
+ .await?;
|
||||
+ tracing::info!("⚙️ Fixed fourth migration");
|
||||
+ let elapsed = started.elapsed();
|
||||
+ tracing::info!(
|
||||
+ "✅ Fixed all known modrinth-issued migrations in {:.2?}",
|
||||
+ elapsed
|
||||
+ );
|
||||
Ok(())
|
||||
}
|
||||
32
pnpm-lock.yaml
generated
32
pnpm-lock.yaml
generated
@@ -487,6 +487,9 @@ importers:
|
||||
'@tresjs/core':
|
||||
specifier: ^4.3.4
|
||||
version: 4.3.6(three@0.172.0)(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4))
|
||||
'@tresjs/post-processing':
|
||||
specifier: ^2.4.0
|
||||
version: 2.4.0(@tresjs/core@4.3.6(three@0.172.0)(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4)))(three@0.172.0)(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4))
|
||||
'@types/markdown-it':
|
||||
specifier: ^14.1.1
|
||||
version: 14.1.1
|
||||
@@ -514,6 +517,9 @@ importers:
|
||||
markdown-it:
|
||||
specifier: ^13.0.2
|
||||
version: 13.0.2
|
||||
postprocessing:
|
||||
specifier: ^6.37.6
|
||||
version: 6.37.6(three@0.172.0)
|
||||
qrcode.vue:
|
||||
specifier: ^3.4.1
|
||||
version: 3.4.1(vue@3.5.13(typescript@5.5.4))
|
||||
@@ -2558,6 +2564,13 @@ packages:
|
||||
three: '>=0.133'
|
||||
vue: '>=3.4'
|
||||
|
||||
'@tresjs/post-processing@2.4.0':
|
||||
resolution: {integrity: sha512-4l18DTLkn0Y/abyn+FD/gSJ6/SC01oXn+/qPgUxMgxZ8zGaw4PZbOi4yorhbSbOTp0gO4D1X7lNOvNUokqJwFw==}
|
||||
peerDependencies:
|
||||
'@tresjs/core': '>=4.0'
|
||||
three: '>=0.169'
|
||||
vue: '>=3.4'
|
||||
|
||||
'@trysound/sax@0.2.0':
|
||||
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@@ -6450,6 +6463,11 @@ packages:
|
||||
posthog-js@1.158.2:
|
||||
resolution: {integrity: sha512-ovb7GHHRNDf6vmuL+8lbDukewzDzQlLZXg3d475hrfHSBgidYeTxtLGtoBcUz4x6558BLDFjnSip+f3m4rV9LA==}
|
||||
|
||||
postprocessing@6.37.6:
|
||||
resolution: {integrity: sha512-KrdKLf1257RkoIk3z3nhRS0aToKrX2xJgtR0lbnOQUjd+1I4GVNv1gQYsQlfRglvEXjpzrwqOA5fXfoDBimadg==}
|
||||
peerDependencies:
|
||||
three: '>= 0.157.0 < 0.179.0'
|
||||
|
||||
potpack@1.0.2:
|
||||
resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==}
|
||||
|
||||
@@ -10455,6 +10473,16 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@tresjs/post-processing@2.4.0(@tresjs/core@4.3.6(three@0.172.0)(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4)))(three@0.172.0)(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4))':
|
||||
dependencies:
|
||||
'@tresjs/core': 4.3.6(three@0.172.0)(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4))
|
||||
'@vueuse/core': 12.8.2(typescript@5.5.4)
|
||||
postprocessing: 6.37.6(three@0.172.0)
|
||||
three: 0.172.0
|
||||
vue: 3.5.13(typescript@5.5.4)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@trysound/sax@0.2.0': {}
|
||||
|
||||
'@tweenjs/tween.js@23.1.3': {}
|
||||
@@ -15670,6 +15698,10 @@ snapshots:
|
||||
preact: 10.23.2
|
||||
web-vitals: 4.2.3
|
||||
|
||||
postprocessing@6.37.6(three@0.172.0):
|
||||
dependencies:
|
||||
three: 0.172.0
|
||||
|
||||
potpack@1.0.2: {}
|
||||
|
||||
preact@10.23.2: {}
|
||||
|
||||
Reference in New Issue
Block a user