18 Commits

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

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

* fix: performance improvements with cache loading+saving

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

* feat: antialiasing

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

* fix: lint issues

* fix: lint issues

* feat: tweaks to radial spotlight

* fix: app nav btn colors
2025-07-09 21:41:36 +00:00
Nitrrine 3c79607d1f feat(app): increase logs card height (#3953) 2025-07-09 21:39:51 +00:00
42 changed files with 5608 additions and 1710 deletions
+47 -17
View File
@@ -16,6 +16,7 @@ on:
- 'packages/assets/**' - 'packages/assets/**'
- 'packages/ui/**' - 'packages/ui/**'
- 'packages/utils/**' - 'packages/utils/**'
workflow_dispatch:
jobs: jobs:
build: build:
@@ -24,12 +25,12 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
# platform: [macos-latest, windows-latest, ubuntu-latest] # platform: [macos-latest, windows-latest, ubuntu-latest]
platform: [ubuntu-latest] platform: [windows-latest, ubuntu-latest]
include: include:
# - platform: macos-latest # - platform: macos-latest
# artifact-target-name: universal-apple-darwin # artifact-target-name: universal-apple-darwin
# - platform: windows-latest - platform: windows-latest
# artifact-target-name: x86_64-pc-windows-msvc artifact-target-name: x86_64-pc-windows-msvc
- platform: ubuntu-latest - platform: ubuntu-latest
artifact-target-name: x86_64-unknown-linux-gnu artifact-target-name: x86_64-unknown-linux-gnu
@@ -41,6 +42,35 @@ jobs:
with: with:
fetch-depth: 2 fetch-depth: 2
- name: 🔍 Validate Git config does not introduce CRLF
shell: bash
run: |
echo "🔍 Checking Git config for CRLF settings..."
autocrlf=$(git config --get core.autocrlf || echo "unset")
eol_setting=$(git config --get core.eol || echo "unset")
echo "core.autocrlf = $autocrlf"
echo "core.eol = $eol_setting"
if [ "$autocrlf" = "true" ]; then
echo "⚠️ WARNING: core.autocrlf is set to 'true'. Consider setting it to 'input' or 'false'."
fi
if [ "$eol_setting" = "crlf" ]; then
echo "⚠️ WARNING: core.eol is set to 'crlf'. Consider unsetting it or setting to 'lf'."
fi
- name: 🔍 Check migration files line endings (LF only)
shell: bash
run: |
echo "🔍 Scanning migration SQL files for CR characters (\\r)..."
if grep -Iq $'\r' packages/app-lib/migrations/*.sql; then
echo "❌ ERROR: Some migration files contain CR (\\r) characters — expected only LF line endings."
exit 1
fi
echo "✅ All migration files use LF line endings"
- name: 🧰 Setup Rust toolchain - name: 🧰 Setup Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1 uses: actions-rust-lang/setup-rust-toolchain@v1
with: with:
@@ -73,11 +103,11 @@ jobs:
- name: 🧰 Install dependencies - name: 🧰 Install dependencies
run: pnpm install run: pnpm install
# - name: ✍️ Set up Windows code signing (jsign) - name: ✍️ Set up Windows code signing (jsign)
# if: matrix.platform == 'windows-latest' && env.SIGN_WINDOWS_BINARIES == 'true' if: matrix.platform == 'windows-latest' && env.SIGN_WINDOWS_BINARIES == 'true'
# shell: bash shell: bash
# run: | run: |
# choco install jsign --ignore-dependencies choco install jsign --ignore-dependencies
- name: 🗑️ Clean up cached bundles - name: 🗑️ Clean up cached bundles
shell: bash shell: bash
@@ -99,15 +129,15 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
# - name: 🔨 Build Windows app - name: 🔨 Build Windows app
# if: matrix.platform == 'windows-latest' if: matrix.platform == 'windows-latest'
# shell: pwsh shell: pwsh
# run: | run: |
# $env:JAVA_HOME = "$env:JAVA_HOME_11_X64" $env:JAVA_HOME = "$env:JAVA_HOME_11_X64"
# pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis' pnpm --filter=@modrinth/app run tauri build --config tauri-release.conf.json --verbose --bundles 'nsis'
# env: env:
# TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
# TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
- name: 📤 Upload app bundles - name: 📤 Upload app bundles
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
+22 -7
View File
@@ -42,7 +42,7 @@ import ModrinthLoadingIndicator from '@/components/LoadingIndicatorBar.vue'
import { handleError, useNotifications } from '@/store/notifications.js' import { handleError, useNotifications } from '@/store/notifications.js'
import { command_listener, warning_listener } from '@/helpers/events.js' import { command_listener, warning_listener } from '@/helpers/events.js'
import { type } from '@tauri-apps/plugin-os' import { type } from '@tauri-apps/plugin-os'
import { getOS, isDev, restartApp } from '@/helpers/utils.js' import { getOS, isDev } from '@/helpers/utils.js'
import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics' import { debugAnalytics, initAnalytics, optOutAnalytics, trackEvent } from '@/helpers/analytics'
import { getCurrentWindow } from '@tauri-apps/api/window' import { getCurrentWindow } from '@tauri-apps/api/window'
import { getVersion } from '@tauri-apps/api/app' import { getVersion } from '@tauri-apps/api/app'
@@ -72,6 +72,9 @@ import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import { get_available_capes, get_available_skins } from './helpers/skins' import { get_available_capes, get_available_skins } from './helpers/skins'
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer' import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
// [AR] Feature
import { getRemote, updateState } from '@/helpers/update.js'
const themeStore = useTheming() const themeStore = useTheming()
const news = ref([]) const news = ref([])
@@ -99,6 +102,7 @@ const isMaximized = ref(false)
onMounted(async () => { onMounted(async () => {
await useCheckDisableMouseover() await useCheckDisableMouseover()
await getRemote(false) // [AR] Check for updates
document.querySelector('body').addEventListener('click', handleClick) document.querySelector('body').addEventListener('click', handleClick)
document.querySelector('body').addEventListener('auxclick', handleAuxClick) document.querySelector('body').addEventListener('auxclick', handleAuxClick)
@@ -161,11 +165,11 @@ async function setupApp() {
initAnalytics() initAnalytics()
if (!telemetry) { if (!telemetry) {
console.info("[AR] Telemetry disabled by default (Hard patched).") console.info("[AR] Telemetry disabled by default (Hard patched).")
optOutAnalytics() optOutAnalytics()
} }
if (!personalized_ads) { if (!personalized_ads) {
console.info("[AR] Personalized ads disabled by default (Hard patched).") console.info("[AR] Personalized ads disabled by default (Hard patched).")
} }
if (dev) debugAnalytics() if (dev) debugAnalytics()
@@ -188,7 +192,7 @@ async function setupApp() {
}), }),
) )
// Patched by AstralRinth /// [AR] Patch
// useFetch( // useFetch(
// `https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`, // `https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`,
// 'criticalAnnouncements', // 'criticalAnnouncements',
@@ -465,12 +469,20 @@ function handleAuxClick(e) {
<PlusIcon /> <PlusIcon />
</NavButton> </NavButton>
<div class="flex flex-grow"></div> <div class="flex flex-grow"></div>
<NavButton v-if="updateAvailable" v-tooltip.right="'Install update'" :to="() => restartApp()"> <!-- [AR] TODO -->
<!-- <NavButton v-if="updateAvailable" v-tooltip.right="'Install update'" :to="() => restartApp()">
<DownloadIcon /> <DownloadIcon />
</NavButton> -->
<template v-if="updateState">
<NavButton class="neon-icon pulse" v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
<SettingsIcon />
</NavButton> </NavButton>
</template>
<template v-else>
<NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()"> <NavButton v-tooltip.right="'Settings'" :to="() => $refs.settingsModal.show()">
<SettingsIcon /> <SettingsIcon />
</NavButton> </NavButton>
</template>
<ButtonStyled v-if="credentials" type="transparent" circular> <ButtonStyled v-if="credentials" type="transparent" circular>
<OverflowMenu <OverflowMenu
:options="[ :options="[
@@ -501,13 +513,13 @@ function handleAuxClick(e) {
<!-- <ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" /> --> <!-- <ModrinthAppLogo class="h-full w-auto text-contrast pointer-events-none" /> -->
<div class="flex items-center gap-1 ml-3"> <div class="flex items-center gap-1 ml-3">
<button <button
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all" class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
@click="router.back()" @click="router.back()"
> >
<LeftArrowIcon /> <LeftArrowIcon />
</button> </button>
<button <button
class="cursor-pointer p-0 m-0 border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all" class="cursor-pointer p-0 m-0 text-contrast border-none outline-none bg-button-bg rounded-full flex items-center justify-center w-6 h-6 hover:brightness-75 transition-all"
@click="router.forward()" @click="router.forward()"
> >
<RightArrowIcon /> <RightArrowIcon />
@@ -659,6 +671,9 @@ function handleAuxClick(e) {
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../../packages/assets/styles/neon-icon.scss';
@import '../../../packages/assets/styles/neon-text.scss';
.window-controls { .window-controls {
z-index: 20; z-index: 20;
display: none; display: none;
@@ -153,11 +153,11 @@ const loginErrorModal = ref(null)
const unexpectedErrorModal = ref(null) const unexpectedErrorModal = ref(null)
const playerName = ref('') const playerName = ref('')
async function tryOfflineLogin() { // Patched by AstralRinth async function tryOfflineLogin() { // [AR] Feature
loginOfflineModal.value.show() loginOfflineModal.value.show()
} }
async function offlineLoginFinally() { // Patched by AstralRinth async function offlineLoginFinally() { // [AR] Feature
const name = playerName.value const name = playerName.value
if (name.length > 1 && name.length < 20 && name !== '') { if (name.length > 1 && name.length < 20 && name !== '') {
const loggedIn = await offline_login(name).catch(handleError) const loggedIn = await offline_login(name).catch(handleError)
@@ -176,12 +176,12 @@ async function offlineLoginFinally() { // Patched by AstralRinth
} }
} }
function retryOfflineLogin() { // Patched by AstralRinth function retryOfflineLogin() { // [AR] Feature
loginErrorModal.value.hide() loginErrorModal.value.hide()
tryOfflineLogin() tryOfflineLogin()
} }
function getAccountType(account) { // Patched by AstralRinth function getAccountType(account) { // [AR] Feature
if (account.access_token != "null" && account.access_token != null && account.access_token != "") { if (account.access_token != "null" && account.access_token != null && account.access_token != "") {
return License return License
} else { } else {
@@ -18,11 +18,16 @@ import { cancel_directory_change } from '@/helpers/settings.ts'
import { install } from '@/helpers/profile.js' import { install } from '@/helpers/profile.js'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { applyMigrationFix } from '@/helpers/utils.js'
import { restartApp } from '@/helpers/utils.js'
const errorModal = ref() const errorModal = ref()
const error = ref() const error = ref()
const closable = ref(true) const closable = ref(true)
const errorCollapsed = ref(false) const errorCollapsed = ref(false)
const language = ref('en')
const migrationFixSuccess = ref(null) // null | true | false
const migrationFixCallbackModel = ref()
const title = ref('An error occurred') const title = ref('An error occurred')
const errorType = ref('unknown') const errorType = ref('unknown')
@@ -148,6 +153,30 @@ async function copyToClipboard(text) {
copied.value = false copied.value = false
}, 3000) }, 3000)
} }
function toggleLanguage() {
language.value = language.value === 'en' ? 'ru' : 'en'
}
async function onApplyMigrationFix(eol) {
console.log(`[AR] • Attempting to apply migration ${eol.toUpperCase()} fix`)
try {
const result = await applyMigrationFix(eol)
migrationFixSuccess.value = result === true
console.log(`[AR] • Successfully applied migration ${eol.toUpperCase()} fix`, result)
} catch (err) {
console.error(`[AR] • Failed to apply migration fix:`, err)
migrationFixSuccess.value = false
} finally {
migrationFixCallbackModel.value?.show?.()
if (migrationFixSuccess.value === true) {
setTimeout(async () => {
await restartApp()
}, 3000)
}
}
}
</script> </script>
<template> <template>
@@ -298,10 +327,20 @@ async function copyToClipboard(text) {
<template v-if="copied"> <CheckIcon class="text-green" /> Copied! </template> <template v-if="copied"> <CheckIcon class="text-green" /> Copied! </template>
<template v-else> <CopyIcon /> Copy debug info </template> <template v-else> <CopyIcon /> Copy debug info </template>
</button> </button>
<ButtonStyled class="neon-button neon">
<a href="https://me.astralium.su/get/ar/help" target="_blank" rel="noopener noreferrer">
Get AstralRinth support
</a>
</ButtonStyled>
<ButtonStyled class="neon-button neon" >
<a href="https://me.astralium.su/get/ar" target="_blank" rel="noopener noreferrer">
Checkout latest releases
</a>
</ButtonStyled>
</ButtonStyled> </ButtonStyled>
</div> </div>
<template v-if="hasDebugInfo"> <template v-if="hasDebugInfo">
<div class="bg-button-bg rounded-xl mt-2 overflow-clip"> <div class="bg-button-bg rounded-xl mt-2 overflow-hidden">
<button <button
class="flex items-center justify-between w-full bg-transparent border-0 px-4 py-3 cursor-pointer" class="flex items-center justify-between w-full bg-transparent border-0 px-4 py-3 cursor-pointer"
@click="errorCollapsed = !errorCollapsed" @click="errorCollapsed = !errorCollapsed"
@@ -313,10 +352,121 @@ async function copyToClipboard(text) {
/> />
</button> </button>
<Collapsible :collapsed="errorCollapsed"> <Collapsible :collapsed="errorCollapsed">
<pre class="m-0 px-4 py-3 bg-bg rounded-none">{{ debugInfo }}</pre> <pre
class="m-0 px-4 py-3 bg-bg rounded-none whitespace-pre-wrap break-words overflow-x-auto max-w-full"
>{{ debugInfo }}</pre>
</Collapsible> </Collapsible>
</div> </div>
<template v-if="errorType === 'state_init'">
<div class="notice">
<div class="flex justify-between items-center">
<h3 v-if="language === 'en'" class="notice__title"> Migration Issue Important Notice </h3>
<h3 v-if="language === 'ru'" class="notice__title"> Проблема миграции Важное уведомление </h3>
<ButtonStyled>
<button @click="toggleLanguage">
{{ language === 'en' ? '📖 Русский' : '📖 English' }}
</button>
</ButtonStyled>
</div>
<p v-if="language === 'en'" class="notice__text">
We're experiencing an issue with our database migration system due to differences in how different operating systems handle line endings. This might cause problems with our app's functionality.
</p>
<p v-if="language === 'en'" class="notice__text">
<strong>What's happening?</strong> When we build our app, we use a system that checks the integrity of our database migrations. However, this system can get confused when it encounters different line endings (like CRLF vs LF) used by different operating systems. This can lead to errors and make our app unusable.
</p>
<p v-if="language === 'en'" class="notice__text">
<strong>Why is this happening?</strong> This issue is caused by a combination of factors, including different operating systems handling line endings differently, Git's line ending conversion settings, and our app's build process.
</p>
<p v-if="language === 'en'" class="notice__text">
<strong>What are we doing about it?</strong> We're working to resolve this issue and ensure that our app works smoothly for all users. In the meantime, we apologize for any inconvenience this might cause and appreciate your patience and understanding.
</p>
<p v-if="language === 'ru'" class="notice__text">
Мы сталкиваемся с проблемой в нашей системе миграции базы данных из-за различий в том, как разные операционные системы обрабатывают окончания строк. Это может вызвать проблемы с функциональностью нашего приложения.
</p>
<p v-if="language === 'ru'" class="notice__text">
<strong>Что происходит?</strong> Когда мы строим наше приложение, мы используем систему, которая проверяет целостность наших миграций базы данных. Однако эта система может сбиваться, когда сталкивается с различными окончаниями строк (например, CRLF против LF), используемыми разными операционными системами. Это может привести к ошибкам и сделать наше приложение неработоспособным.
</p>
<p v-if="language === 'ru'" class="notice__text">
<strong>Почему это происходит?</strong> Эта проблема вызвана сочетанием факторов, включая различную обработку окончаний строк разными операционными системами, настройки преобразования окончаний строк в Git и процесс сборки нашего приложения.
</p>
<p v-if="language === 'ru'" class="notice__text">
<strong>Что мы с этим делаем?</strong> Мы работаем над решением этой проблемы и обеспечением бесперебойной работы нашего приложения для всех пользователей. В это время мы извиняемся за возможные неудобства и благодарим вас за терпение и понимание.
</p>
</div>
<h2 class="text-lg font-bold text-contrast">
<template v-if="language === 'en'">Possible fix in real time:</template>
<template v-if="language === 'ru'">Возможное исправление в реальном времени:</template>
</h2>
<div class="flex justify-between">
<ol class="flex flex-col gap-3">
<li>
<ButtonStyled class="neon-button neon">
<button
:title="language === 'en'
? 'Convert all line endings in migration files to LF (Unix-style: \\n)'
: 'Преобразовать все окончания строк в файлах миграций в LF (Unix-стиль: \\n)'"
aria-label="LF"
@click="onApplyMigrationFix('lf')"
>
{{ language === 'en' ? 'Apply LF Migration Fix' : 'Применить исправление миграции LF' }}
</button>
</ButtonStyled>
</li>
<li>
<ButtonStyled class="neon-button neon">
<button
:title="language === 'en'
? 'Convert all line endings in migration files to CRLF (Windows-style: \\r\\n)'
: 'Преобразовать все окончания строк в файлах миграций в CRLF (Windows-стиль: \\r\\n)'"
aria-label="CRLF"
@click="onApplyMigrationFix('crlf')"
>
{{ language === 'en' ? 'Apply CRLF Migration Fix' : 'Применить исправление миграции CRLF' }}
</button>
</ButtonStyled>
</li>
</ol>
</div>
</template> </template>
</template>
</div>
</ModalWrapper>
<ModalWrapper
ref="migrationFixCallbackModel"
:header="language === 'en'
? '💡 Migration fix report'
: '💡 Отчет об исправлении миграции'"
:closable="closable">
<div class="modal-body">
<h2 class="text-lg font-bold text-contrast space-y-2">
<template v-if="migrationFixSuccess === true">
<p class="flex items-center gap-2 neon-text">
{{ language === 'en'
? 'The migration fix has been applied successfully. Please restart the launcher and try to log in to the game :)'
: 'Исправление миграции успешно применено. Пожалуйста, перезапустите лаунчер и попробуйте снова авторизоваться в игре :)' }}
</p>
<p class="mt-2 text-sm neon-text">
{{ language === 'en'
? 'If the problem persists, please try the other fix.'
: 'Если проблема сохраняется, пожалуйста, попробуйте другой способ.' }}
</p>
</template>
<template v-else-if="migrationFixSuccess === false">
<p class="flex items-center gap-2 neon-text">
{{ language === 'en'
? 'The migration fix failed or had no effect.'
: 'Исправление миграции не было успешно применено или не имело эффекта.' }}
</p>
<p class="mt-2 text-sm neon-text">
{{ language === 'en'
? 'If the problem persists, please try the other fix.'
: 'Если проблема сохраняется, пожалуйста, попробуйте другой способ.' }}
</p>
</template>
</h2>
</div> </div>
</ModalWrapper> </ModalWrapper>
</template> </template>
@@ -333,6 +483,9 @@ async function copyToClipboard(text) {
</style> </style>
<style scoped lang="scss"> <style scoped lang="scss">
@import '../../../../../packages/assets/styles/neon-button.scss';
@import '../../../../../packages/assets/styles/neon-text.scss';
.cta-button { .cta-button {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -36,60 +36,6 @@
<span class="circle stopped" /> <span class="circle stopped" />
<span class="running-text"> No instances running </span> <span class="running-text"> No instances running </span>
</div> </div>
<div v-if="updateState">
<a>
<Button class="download" :disabled="installState" @click="initUpdateModal(), getRemote(false)">
<DownloadIcon />
{{
installState
? "Downloading new update..."
: "Download new update"
}}
</Button>
</a>
</div>
<ModalWrapper ref="updateModalView" :has-to-type="false" header="Request to update the AstralRinth launcher">
<div class="modal-body">
<div class="markdown-body">
<p>The new version of the AstralRinth launcher is available.</p>
<p>Your version is outdated. We recommend that you update to the latest version.</p>
<p><strong> Warning </strong></p>
<p>
Before updating, make sure that you have saved all running instances and made a backup copy of the instances
that are valuable to you. Remember that the authors of the product are not responsible for the breakdown of
your files, so you should always make copies of them and keep them in a safe place.
</p>
</div>
<span>Source Git Astralium</span>
<span>Version on remote server <p id="releaseData" class="cosmic inline-fix"></p></span>
<span>Version on local device
<p class="cosmic inline-fix">v{{ version }}</p>
</span>
<div class="button-group push-right">
<Button class="updater-modal" @click="updateModalView.hide()">
Cancel</Button>
<Button class="updater-modal" @click="initDownload()">
Download file
</Button>
</div>
</div>
</ModalWrapper>
<ModalWrapper ref="updateRequestFailView" :has-to-type="false" header="Failed to request a file from the server :(">
<div class="modal-body">
<div class="markdown-body">
<p><strong>Error occurred</strong></p>
<p>Unfortunately, the program was unable to download the file from our servers.</p>
<p>Please try downloading it yourself from <a href="https://me.astralium.su/get/ar" target="_blank" rel="noopener noreferrer">Git Astralium</a> if there are any updates available.</p>
</div>
<span>Local AstralRinth
<p class="cosmic inline-fix">v{{ version }}</p>
</span>
</div>
<div class="button-group push-right">
<Button class="updater-modal" @click="updateRequestFailView.hide()">
Close</Button>
</div>
</ModalWrapper>
</div> </div>
<transition name="download"> <transition name="download">
<Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card"> <Card v-if="showCard === true && currentLoadingBars.length > 0" ref="card" class="info-card">
@@ -138,29 +84,6 @@ import ProgressBar from '@/components/ui/ProgressBar.vue'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { get_many } from '@/helpers/profile.js' import { get_many } from '@/helpers/profile.js'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import { getVersion } from '@tauri-apps/api/app'
const version = await getVersion()
import { installState, getRemote, updateState } from '@/helpers/update.js'
import ModalWrapper from './modal/ModalWrapper.vue'
const updateModalView = ref(null)
const updateRequestFailView = ref(null)
const initUpdateModal = async () => {
updateModalView.value.show()
}
const initDownload = async () => {
updateModalView.value.hide()
const result = await getRemote(true);
if (!result) {
updateRequestFailView.value.show()
}
}
await getRemote(false)
const router = useRouter() const router = useRouter()
const card = ref(null) const card = ref(null)
@@ -318,101 +241,6 @@ onBeforeUnmount(() => {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.inline-fix {
display: inline-flex;
margin-top: -2rem;
margin-bottom: -2rem;
//margin-left: 0.3rem;
}
.cosmic {
color: #3e8cde;
text-decoration: none;
text-shadow:
0 0 4px rgba(79, 173, 255, 0.5),
0 0 8px rgba(14, 98, 204, 0.5),
0 0 12px rgba(122, 31, 199, 0.5);
transition: color 0.35s ease;
}
.markdown-body {
:deep(table) {
width: auto;
}
:deep(hr),
:deep(h1),
:deep(h2) {
max-width: max(60rem, 90%);
}
:deep(ul),
:deep(ol) {
margin-left: 2rem;
}
}
.modal-body {
display: flex;
flex-direction: column;
gap: 1rem;
padding: var(--gap-lg);
text-align: left;
.button-group {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
}
strong {
color: var(--color-contrast);
}
}
.download {
color: #3e8cde;
border-radius: var(--radius-md);
border: 1px solid var(--color-button-bg);
// padding: var(--gap-sm) var(--gap-lg);
background-color: rgba(0, 0, 0, 0);
text-decoration: none;
text-shadow:
0 0 4px rgba(79, 173, 255, 0.5),
0 0 8px rgba(14, 98, 204, 0.5),
0 0 12px rgba(122, 31, 199, 0.5);
transition: color 0.35s ease;
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
.download:hover,
.download:focus,
.download:active {
color: #10fae5;
text-shadow: #26065e;
}
.updater-modal {
color: #3e8cde;
padding: var(--gap-sm) var(--gap-lg);
text-decoration: none;
text-shadow:
0 0 4px rgba(79, 173, 255, 0.5),
0 0 8px rgba(14, 98, 204, 0.5),
0 0 12px rgba(122, 31, 199, 0.5);
transition: color 0.35s ease;
}
.updater-modal:hover,
.updater-modal:focus,
.updater-modal:active {
color: #10fae5;
text-shadow: #26065e;
}
.action-groups { .action-groups {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -8,6 +8,8 @@ import {
PaintbrushIcon, PaintbrushIcon,
GameIcon, GameIcon,
CoffeeIcon, CoffeeIcon,
DownloadIcon,
SpinnerIcon,
} from '@modrinth/assets' } from '@modrinth/assets'
import { TabbedModal } from '@modrinth/ui' import { TabbedModal } from '@modrinth/ui'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
@@ -23,6 +25,23 @@ import { useTheming } from '@/store/state'
import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue' import FeatureFlagSettings from '@/components/ui/settings/FeatureFlagSettings.vue'
import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue' import ModalWrapper from '@/components/ui/modal/ModalWrapper.vue'
import { get, set } from '@/helpers/settings.ts' import { get, set } from '@/helpers/settings.ts'
// [AR] Imports
import { installState, getRemote, updateState } from '@/helpers/update.js'
const updateModalView = ref(null)
const updateRequestFailView = ref(null)
const initUpdateModal = async () => {
updateModalView.value.show()
}
const initDownload = async () => {
updateModalView.value.hide()
const result = await getRemote(true);
if (!result) {
updateRequestFailView.value.show()
}
}
const themeStore = useTheming() const themeStore = useTheming()
@@ -141,8 +160,7 @@ function devModeCount() {
<button <button
class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation" class="p-0 m-0 bg-transparent border-none cursor-pointer button-animation"
:class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }" :class="{ 'text-brand': themeStore.devMode, 'text-secondary': !themeStore.devMode }"
@click="devModeCount" @click="devModeCount">
>
<AstralRinthLogo class="w-6 h-6" /> <AstralRinthLogo class="w-6 h-6" />
</button> </button>
<div> <div>
@@ -153,9 +171,80 @@ function devModeCount() {
{{ osVersion }} {{ osVersion }}
</p> </p>
</div> </div>
<div v-if="updateState" class="w-8 h-8 cursor-pointer hover:brightness-75 neon-icon pulse">
<template v-if="installState">
<SpinnerIcon class="size-6 animate-spin" v-tooltip.bottom="'Installing in process...'" />
</template>
<template v-else>
<DownloadIcon class="size-6" v-tooltip.bottom="'View update info'" @click="!installState && (initUpdateModal(), getRemote(false))" />
</template>
</div>
</div> </div>
</div> </div>
</template> </template>
</TabbedModal> </TabbedModal>
<!-- [AR] Feature -->
<ModalWrapper ref="updateModalView" :has-to-type="false" header="Request to update the AstralRinth launcher">
<div class="space-y-4">
<div class="space-y-2">
<p>The new version of the AstralRinth launcher is available.</p>
<p>Your version is outdated. We recommend that you update to the latest version.</p>
<p><strong> Warning </strong></p>
<p>
Before updating, make sure that you have saved all running instances and made a backup copy of the instances
that are valuable to you. Remember that the authors of the product are not responsible for the breakdown of
your files, so you should always make copies of them and keep them in a safe place.
</p>
</div>
<div class="text-sm text-secondary space-y-1">
<a class="neon-text" href="https://me.astralium.su/get/ar" target="_blank"
rel="noopener noreferrer"><strong>Source:</strong> Git Astralium</a>
<p>
<strong>Version on remote server:</strong>
<span id="releaseData" class="neon-text"></span>
</p>
<p>
<strong>Version on local device:</strong>
<span class="neon-text">v{{ version }}</span>
</p>
</div>
<div class="absolute bottom-4 right-4 flex items-center gap-4 neon-button neon">
<Button class="bordered" @click="updateModalView.hide()">Cancel</Button>
<Button class="bordered" @click="initDownload()">Download file</Button>
</div>
</div>
</ModalWrapper>
<ModalWrapper ref="updateRequestFailView" :has-to-type="false" header="Failed to request a file from the server :(">
<div class="space-y-4">
<div class="space-y-2">
<p><strong>Error occurred</strong></p>
<p>Unfortunately, the program was unable to download the file from our servers.</p>
<p>
Please try downloading it yourself from
<a class="neon-text" href="https://me.astralium.su/get/ar" target="_blank" rel="noopener noreferrer">Git
Astralium</a>
if there are any updates available.
</p>
</div>
<div class="text-sm text-secondary">
<p>
<strong>Local AstralRinth:</strong>
<span class="neon-text">v{{ version }}</span>
</p>
</div>
<div class="absolute bottom-4 right-4 flex items-center gap-4 neon-button neon">
<Button class="bordered" @click="updateRequestFailView.hide()">Close</Button>
</div>
</div>
</ModalWrapper>
</ModalWrapper> </ModalWrapper>
</template> </template>
<style lang="scss" scoped>
@import '../../../../../../packages/assets/styles/neon-icon.scss';
@import '../../../../../../packages/assets/styles/neon-button.scss';
@import '../../../../../../packages/assets/styles/neon-text.scss';
</style>
@@ -30,7 +30,7 @@ watch(
option, you opt out and ads will no longer be shown based on your interests. option, you opt out and ads will no longer be shown based on your interests.
</p> </p>
</div> </div>
<!-- AstralRinth disabled element by default --> <!-- [AR] Patch. Disabled element by default -->
<Toggle id="personalized-ads" v-model="settings.personalized_ads" :disabled="!settings.personalized_ads" /> <Toggle id="personalized-ads" v-model="settings.personalized_ads" :disabled="!settings.personalized_ads" />
</div> </div>
@@ -43,7 +43,7 @@ watch(
longer be collected. longer be collected.
</p> </p>
</div> </div>
<!-- AstralRinth disabled element by default --> <!-- [AR] Patch. Disabled element by default -->
<Toggle id="opt-out-analytics" v-model="settings.telemetry" :disabled="!settings.telemetry" /> <Toggle id="opt-out-analytics" v-model="settings.telemetry" :disabled="!settings.telemetry" />
</div> </div>
@@ -2,25 +2,40 @@ import * as THREE from 'three'
import type { Skin, Cape } from '../skins' import type { Skin, Cape } from '../skins'
import { get_normalized_skin_texture, determineModelType } from '../skins' import { get_normalized_skin_texture, determineModelType } from '../skins'
import { reactive } from 'vue' import { reactive } from 'vue'
import { setupSkinModel, disposeCaches } from '@modrinth/utils' import { setupSkinModel, disposeCaches, loadTexture, applyCapeTexture } from '@modrinth/utils'
import { skinPreviewStorage } from '../storage/skin-preview-storage' import { skinPreviewStorage } from '../storage/skin-preview-storage'
import { CapeModel, ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets' import { headStorage } from '../storage/head-storage'
import { ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
export interface RenderResult { export interface RenderResult {
forwards: string forwards: string
backwards: string backwards: string
} }
export interface RawRenderResult {
forwards: Blob
backwards: Blob
}
class BatchSkinRenderer { class BatchSkinRenderer {
private renderer: THREE.WebGLRenderer private renderer: THREE.WebGLRenderer | null = null
private readonly scene: THREE.Scene private scene: THREE.Scene | null = null
private readonly camera: THREE.PerspectiveCamera private camera: THREE.PerspectiveCamera | null = null
private currentModel: THREE.Group | null = null private currentModel: THREE.Group | null = null
private readonly width: number
private readonly height: number
constructor(width: number = 360, height: number = 504) { constructor(width: number = 360, height: number = 504) {
this.width = width
this.height = height
}
private initializeRenderer(): void {
if (this.renderer) return
const canvas = document.createElement('canvas') const canvas = document.createElement('canvas')
canvas.width = width canvas.width = this.width
canvas.height = height canvas.height = this.height
this.renderer = new THREE.WebGLRenderer({ this.renderer = new THREE.WebGLRenderer({
canvas: canvas, canvas: canvas,
@@ -33,10 +48,10 @@ class BatchSkinRenderer {
this.renderer.toneMapping = THREE.NoToneMapping this.renderer.toneMapping = THREE.NoToneMapping
this.renderer.toneMappingExposure = 10.0 this.renderer.toneMappingExposure = 10.0
this.renderer.setClearColor(0x000000, 0) this.renderer.setClearColor(0x000000, 0)
this.renderer.setSize(width, height) this.renderer.setSize(this.width, this.height)
this.scene = new THREE.Scene() this.scene = new THREE.Scene()
this.camera = new THREE.PerspectiveCamera(20, width / height, 0.4, 1000) this.camera = new THREE.PerspectiveCamera(20, this.width / this.height, 0.4, 1000)
const ambientLight = new THREE.AmbientLight(0xffffff, 2) const ambientLight = new THREE.AmbientLight(0xffffff, 2)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2) const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
@@ -50,9 +65,12 @@ class BatchSkinRenderer {
textureUrl: string, textureUrl: string,
modelUrl: string, modelUrl: string,
capeUrl?: string, capeUrl?: string,
capeModelUrl?: string, ): Promise<RawRenderResult> {
): Promise<RenderResult> { this.initializeRenderer()
await this.setupModel(modelUrl, textureUrl, capeModelUrl, capeUrl)
this.clearScene()
await this.setupModel(modelUrl, textureUrl, capeUrl)
const headPart = this.currentModel!.getObjectByName('Head') const headPart = this.currentModel!.getObjectByName('Head')
let lookAtTarget: [number, number, number] let lookAtTarget: [number, number, number]
@@ -77,35 +95,32 @@ class BatchSkinRenderer {
private async renderView( private async renderView(
cameraPosition: [number, number, number], cameraPosition: [number, number, number],
lookAtPosition: [number, number, number], lookAtPosition: [number, number, number],
): Promise<string> { ): Promise<Blob> {
if (!this.camera || !this.renderer || !this.scene) {
throw new Error('Renderer not initialized')
}
this.camera.position.set(...cameraPosition) this.camera.position.set(...cameraPosition)
this.camera.lookAt(...lookAtPosition) this.camera.lookAt(...lookAtPosition)
this.renderer.render(this.scene, this.camera) this.renderer.render(this.scene, this.camera)
return new Promise<string>((resolve, reject) => { const dataUrl = this.renderer.domElement.toDataURL('image/webp', 0.9)
this.renderer.domElement.toBlob((blob) => { const response = await fetch(dataUrl)
if (blob) { return await response.blob()
const url = URL.createObjectURL(blob)
resolve(url)
} else {
reject(new Error('Failed to create blob from canvas'))
}
}, 'image/png')
})
} }
private async setupModel( private async setupModel(modelUrl: string, textureUrl: string, capeUrl?: string): Promise<void> {
modelUrl: string, if (!this.scene) {
textureUrl: string, throw new Error('Renderer not initialized')
capeModelUrl?: string,
capeUrl?: string,
): Promise<void> {
if (this.currentModel) {
this.scene.remove(this.currentModel)
} }
const { model } = await setupSkinModel(modelUrl, textureUrl, capeModelUrl, capeUrl) const { model } = await setupSkinModel(modelUrl, textureUrl)
if (capeUrl) {
const capeTexture = await loadTexture(capeUrl)
applyCapeTexture(model, capeTexture)
}
const group = new THREE.Group() const group = new THREE.Group()
group.add(model) group.add(model)
@@ -116,8 +131,39 @@ class BatchSkinRenderer {
this.currentModel = group this.currentModel = group
} }
private clearScene(): void {
if (!this.scene) return
while (this.scene.children.length > 0) {
const child = this.scene.children[0]
this.scene.remove(child)
if (child instanceof THREE.Mesh) {
if (child.geometry) child.geometry.dispose()
if (child.material) {
if (Array.isArray(child.material)) {
child.material.forEach((material) => material.dispose())
} else {
child.material.dispose()
}
}
}
}
const ambientLight = new THREE.AmbientLight(0xffffff, 2)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
directionalLight.castShadow = true
directionalLight.position.set(2, 4, 3)
this.scene.add(ambientLight)
this.scene.add(directionalLight)
this.currentModel = null
}
public dispose(): void { public dispose(): void {
if (this.renderer) {
this.renderer.dispose() this.renderer.dispose()
}
disposeCaches() disposeCaches()
} }
} }
@@ -133,10 +179,25 @@ function getModelUrlForVariant(variant: string): string {
} }
} }
export const map = reactive(new Map<string, RenderResult>()) export const skinBlobUrlMap = reactive(new Map<string, RenderResult>())
export const headMap = reactive(new Map<string, string>()) export const headBlobUrlMap = reactive(new Map<string, string>())
const DEBUG_MODE = false const DEBUG_MODE = false
let sharedRenderer: BatchSkinRenderer | null = null
function getSharedRenderer(): BatchSkinRenderer {
if (!sharedRenderer) {
sharedRenderer = new BatchSkinRenderer()
}
return sharedRenderer
}
export function disposeSharedRenderer(): void {
if (sharedRenderer) {
sharedRenderer.dispose()
sharedRenderer = null
}
}
export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> { export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
const validKeys = new Set<string>() const validKeys = new Set<string>()
const validHeadKeys = new Set<string>() const validHeadKeys = new Set<string>()
@@ -150,7 +211,7 @@ export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
try { try {
await skinPreviewStorage.cleanupInvalidKeys(validKeys) await skinPreviewStorage.cleanupInvalidKeys(validKeys)
await skinPreviewStorage.cleanupInvalidKeys(validHeadKeys) await headStorage.cleanupInvalidKeys(validHeadKeys)
} catch (error) { } catch (error) {
console.warn('Failed to cleanup unused skin previews:', error) console.warn('Failed to cleanup unused skin previews:', error)
} }
@@ -229,13 +290,17 @@ export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64)
outputCtx.drawImage(hatCanvas, 0, 0, 8, 8, 0, 0, size, size) outputCtx.drawImage(hatCanvas, 0, 0, 8, 8, 0, 0, size, size)
} }
outputCanvas.toBlob((blob) => { outputCanvas.toBlob(
(blob) => {
if (blob) { if (blob) {
resolve(blob) resolve(blob)
} else { } else {
reject(new Error('Failed to create blob from canvas')) reject(new Error('Failed to create blob from canvas'))
} }
}, 'image/png') },
'image/webp',
0.9,
)
} catch (error) { } catch (error) {
reject(error) reject(error)
} }
@@ -252,35 +317,24 @@ export async function generatePlayerHeadBlob(skinUrl: string, size: number = 64)
async function generateHeadRender(skin: Skin): Promise<string> { async function generateHeadRender(skin: Skin): Promise<string> {
const headKey = `${skin.texture_key}-head` const headKey = `${skin.texture_key}-head`
if (headMap.has(headKey)) { if (headBlobUrlMap.has(headKey)) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
const url = headMap.get(headKey)! const url = headBlobUrlMap.get(headKey)!
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
headMap.delete(headKey) headBlobUrlMap.delete(headKey)
} else { } else {
return headMap.get(headKey)! return headBlobUrlMap.get(headKey)!
} }
} }
try {
const cached = await skinPreviewStorage.retrieve(headKey)
if (cached && typeof cached === 'string') {
headMap.set(headKey, cached)
return cached
}
} catch (error) {
console.warn('Failed to retrieve cached head render:', error)
}
const skinUrl = await get_normalized_skin_texture(skin) const skinUrl = await get_normalized_skin_texture(skin)
const headBlob = await generatePlayerHeadBlob(skinUrl, 64) const headBlob = await generatePlayerHeadBlob(skinUrl, 64)
const headUrl = URL.createObjectURL(headBlob) const headUrl = URL.createObjectURL(headBlob)
headMap.set(headKey, headUrl) headBlobUrlMap.set(headKey, headUrl)
try { try {
// @ts-expect-error - skinPreviewStorage.store expects a RenderResult, but we are storing a string url. await headStorage.store(headKey, headBlob)
await skinPreviewStorage.store(headKey, headUrl)
} catch (error) { } catch (error) {
console.warn('Failed to store head render in persistent storage:', error) console.warn('Failed to store head render in persistent storage:', error)
} }
@@ -293,30 +347,49 @@ export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
} }
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> { export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
const renderer = new BatchSkinRenderer()
try { try {
const skinKeys = skins.map(
(skin) => `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`,
)
const headKeys = skins.map((skin) => `${skin.texture_key}-head`)
const [cachedSkinPreviews, cachedHeadPreviews] = await Promise.all([
skinPreviewStorage.batchRetrieve(skinKeys),
headStorage.batchRetrieve(headKeys),
])
for (let i = 0; i < skins.length; i++) {
const skinKey = skinKeys[i]
const headKey = headKeys[i]
const rawCached = cachedSkinPreviews[skinKey]
if (rawCached) {
const cached: RenderResult = {
forwards: URL.createObjectURL(rawCached.forwards),
backwards: URL.createObjectURL(rawCached.backwards),
}
skinBlobUrlMap.set(skinKey, cached)
}
const cachedHead = cachedHeadPreviews[headKey]
if (cachedHead) {
headBlobUrlMap.set(headKey, URL.createObjectURL(cachedHead))
}
}
for (const skin of skins) { for (const skin of skins) {
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}` const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
if (map.has(key)) { if (skinBlobUrlMap.has(key)) {
if (DEBUG_MODE) { if (DEBUG_MODE) {
const result = map.get(key)! const result = skinBlobUrlMap.get(key)!
URL.revokeObjectURL(result.forwards) URL.revokeObjectURL(result.forwards)
URL.revokeObjectURL(result.backwards) URL.revokeObjectURL(result.backwards)
map.delete(key) skinBlobUrlMap.delete(key)
} else continue } else continue
} }
try { const renderer = getSharedRenderer()
const cached = await skinPreviewStorage.retrieve(key)
if (cached) {
map.set(key, cached)
continue
}
} catch (error) {
console.warn('Failed to retrieve cached skin preview:', error)
}
let variant = skin.variant let variant = skin.variant
if (variant === 'UNKNOWN') { if (variant === 'UNKNOWN') {
@@ -330,25 +403,35 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
const modelUrl = getModelUrlForVariant(variant) const modelUrl = getModelUrlForVariant(variant)
const cape: Cape | undefined = capes.find((_cape) => _cape.id === skin.cape_id) const cape: Cape | undefined = capes.find((_cape) => _cape.id === skin.cape_id)
const renderResult = await renderer.renderSkin( const rawRenderResult = await renderer.renderSkin(
await get_normalized_skin_texture(skin), await get_normalized_skin_texture(skin),
modelUrl, modelUrl,
cape?.texture, cape?.texture,
CapeModel,
) )
map.set(key, renderResult) const renderResult: RenderResult = {
forwards: URL.createObjectURL(rawRenderResult.forwards),
backwards: URL.createObjectURL(rawRenderResult.backwards),
}
skinBlobUrlMap.set(key, renderResult)
try { try {
await skinPreviewStorage.store(key, renderResult) await skinPreviewStorage.store(key, rawRenderResult)
} catch (error) { } catch (error) {
console.warn('Failed to store skin preview in persistent storage:', error) console.warn('Failed to store skin preview in persistent storage:', error)
} }
const headKey = `${skin.texture_key}-head`
if (!headBlobUrlMap.has(headKey)) {
await generateHeadRender(skin) await generateHeadRender(skin)
} }
}
} finally { } finally {
renderer.dispose() disposeSharedRenderer()
await cleanupUnusedPreviews(skins) await cleanupUnusedPreviews(skins)
await skinPreviewStorage.debugCalculateStorage()
await headStorage.debugCalculateStorage()
} }
} }
@@ -0,0 +1,229 @@
interface StoredHead {
blob: Blob
timestamp: number
}
export class HeadStorage {
private dbName = 'head-storage'
private version = 1
private db: IDBDatabase | null = null
async init(): Promise<void> {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version)
request.onerror = () => reject(request.error)
request.onsuccess = () => {
this.db = request.result
resolve()
}
request.onupgradeneeded = () => {
const db = request.result
if (!db.objectStoreNames.contains('heads')) {
db.createObjectStore('heads')
}
}
})
}
async store(key: string, blob: Blob): Promise<void> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['heads'], 'readwrite')
const store = transaction.objectStore('heads')
const storedHead: StoredHead = {
blob,
timestamp: Date.now(),
}
return new Promise((resolve, reject) => {
const request = store.put(storedHead, key)
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}
async retrieve(key: string): Promise<string | null> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['heads'], 'readonly')
const store = transaction.objectStore('heads')
return new Promise((resolve, reject) => {
const request = store.get(key)
request.onsuccess = () => {
const result = request.result as StoredHead | undefined
if (!result) {
resolve(null)
return
}
const url = URL.createObjectURL(result.blob)
resolve(url)
}
request.onerror = () => reject(request.error)
})
}
async batchRetrieve(keys: string[]): Promise<Record<string, Blob | null>> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['heads'], 'readonly')
const store = transaction.objectStore('heads')
const results: Record<string, Blob | null> = {}
return new Promise((resolve, _reject) => {
let completedRequests = 0
if (keys.length === 0) {
resolve(results)
return
}
for (const key of keys) {
const request = store.get(key)
request.onsuccess = () => {
const result = request.result as StoredHead | undefined
if (result) {
results[key] = result.blob
} else {
results[key] = null
}
completedRequests++
if (completedRequests === keys.length) {
resolve(results)
}
}
request.onerror = () => {
results[key] = null
completedRequests++
if (completedRequests === keys.length) {
resolve(results)
}
}
}
})
}
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['heads'], 'readwrite')
const store = transaction.objectStore('heads')
let deletedCount = 0
return new Promise((resolve, reject) => {
const request = store.openCursor()
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
if (cursor) {
const key = cursor.primaryKey as string
if (!validKeys.has(key)) {
const deleteRequest = cursor.delete()
deleteRequest.onsuccess = () => {
deletedCount++
}
deleteRequest.onerror = () => {
console.warn('Failed to delete invalid head entry:', key)
}
}
cursor.continue()
} else {
resolve(deletedCount)
}
}
request.onerror = () => reject(request.error)
})
}
async debugCalculateStorage(): Promise<void> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['heads'], 'readonly')
const store = transaction.objectStore('heads')
let totalSize = 0
let count = 0
const entries: Array<{ key: string; size: number }> = []
return new Promise((resolve, reject) => {
const request = store.openCursor()
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
if (cursor) {
const key = cursor.primaryKey as string
const value = cursor.value as StoredHead
const entrySize = value.blob.size
totalSize += entrySize
count++
entries.push({
key,
size: entrySize,
})
cursor.continue()
} else {
console.group('🗄️ Head Storage Debug Info')
console.log(`Total entries: ${count}`)
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
console.log(
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
)
if (entries.length > 0) {
const sortedEntries = entries.sort((a, b) => b.size - a.size)
console.log(
'Largest entry:',
sortedEntries[0].key,
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
)
console.log(
'Smallest entry:',
sortedEntries[sortedEntries.length - 1].key,
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
)
}
console.groupEnd()
resolve()
}
}
request.onerror = () => reject(request.error)
})
}
async clearAll(): Promise<void> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['heads'], 'readwrite')
const store = transaction.objectStore('heads')
return new Promise((resolve, reject) => {
const request = store.clear()
request.onsuccess = () => resolve()
request.onerror = () => reject(request.error)
})
}
}
export const headStorage = new HeadStorage()
@@ -1,4 +1,4 @@
import type { RenderResult } from '../rendering/batch-skin-renderer' import type { RawRenderResult } from '../rendering/batch-skin-renderer'
interface StoredPreview { interface StoredPreview {
forwards: Blob forwards: Blob
@@ -30,18 +30,15 @@ export class SkinPreviewStorage {
}) })
} }
async store(key: string, result: RenderResult): Promise<void> { async store(key: string, result: RawRenderResult): Promise<void> {
if (!this.db) await this.init() if (!this.db) await this.init()
const forwardsBlob = await fetch(result.forwards).then((r) => r.blob())
const backwardsBlob = await fetch(result.backwards).then((r) => r.blob())
const transaction = this.db!.transaction(['previews'], 'readwrite') const transaction = this.db!.transaction(['previews'], 'readwrite')
const store = transaction.objectStore('previews') const store = transaction.objectStore('previews')
const storedPreview: StoredPreview = { const storedPreview: StoredPreview = {
forwards: forwardsBlob, forwards: result.forwards,
backwards: backwardsBlob, backwards: result.backwards,
timestamp: Date.now(), timestamp: Date.now(),
} }
@@ -53,7 +50,7 @@ export class SkinPreviewStorage {
}) })
} }
async retrieve(key: string): Promise<RenderResult | null> { async retrieve(key: string): Promise<RawRenderResult | null> {
if (!this.db) await this.init() if (!this.db) await this.init()
const transaction = this.db!.transaction(['previews'], 'readonly') const transaction = this.db!.transaction(['previews'], 'readonly')
@@ -70,14 +67,56 @@ export class SkinPreviewStorage {
return return
} }
const forwards = URL.createObjectURL(result.forwards) resolve({ forwards: result.forwards, backwards: result.backwards })
const backwards = URL.createObjectURL(result.backwards)
resolve({ forwards, backwards })
} }
request.onerror = () => reject(request.error) request.onerror = () => reject(request.error)
}) })
} }
async batchRetrieve(keys: string[]): Promise<Record<string, RawRenderResult | null>> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['previews'], 'readonly')
const store = transaction.objectStore('previews')
const results: Record<string, RawRenderResult | null> = {}
return new Promise((resolve, _reject) => {
let completedRequests = 0
if (keys.length === 0) {
resolve(results)
return
}
for (const key of keys) {
const request = store.get(key)
request.onsuccess = () => {
const result = request.result as StoredPreview | undefined
if (result) {
results[key] = { forwards: result.forwards, backwards: result.backwards }
} else {
results[key] = null
}
completedRequests++
if (completedRequests === keys.length) {
resolve(results)
}
}
request.onerror = () => {
results[key] = null
completedRequests++
if (completedRequests === keys.length) {
resolve(results)
}
}
}
})
}
async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> { async cleanupInvalidKeys(validKeys: Set<string>): Promise<number> {
if (!this.db) await this.init() if (!this.db) await this.init()
@@ -113,6 +152,67 @@ export class SkinPreviewStorage {
request.onerror = () => reject(request.error) request.onerror = () => reject(request.error)
}) })
} }
async debugCalculateStorage(): Promise<void> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['previews'], 'readonly')
const store = transaction.objectStore('previews')
let totalSize = 0
let count = 0
const entries: Array<{ key: string; size: number }> = []
return new Promise((resolve, reject) => {
const request = store.openCursor()
request.onsuccess = (event) => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result
if (cursor) {
const key = cursor.primaryKey as string
const value = cursor.value as StoredPreview
const entrySize = value.forwards.size + value.backwards.size
totalSize += entrySize
count++
entries.push({
key,
size: entrySize,
})
cursor.continue()
} else {
console.group('🗄️ Skin Preview Storage Debug Info')
console.log(`Total entries: ${count}`)
console.log(`Total size: ${(totalSize / 1024 / 1024).toFixed(2)} MB`)
console.log(
`Average size per entry: ${count > 0 ? (totalSize / count / 1024).toFixed(2) : 0} KB`,
)
if (entries.length > 0) {
const sortedEntries = entries.sort((a, b) => b.size - a.size)
console.log(
'Largest entry:',
sortedEntries[0].key,
'(' + (sortedEntries[0].size / 1024).toFixed(2) + ' KB)',
)
console.log(
'Smallest entry:',
sortedEntries[sortedEntries.length - 1].key,
'(' + (sortedEntries[sortedEntries.length - 1].size / 1024).toFixed(2) + ' KB)',
)
}
console.groupEnd()
resolve()
}
}
request.onerror = () => reject(request.error)
})
}
} }
export const skinPreviewStorage = new SkinPreviewStorage() export const skinPreviewStorage = new SkinPreviewStorage()
+1 -1
View File
@@ -11,7 +11,7 @@ const releaseLink = `https://git.astralium.su/api/v1/repos/didirus/AstralRinth/r
const failedFetch = [`Failed to fetch remote releases:`, `Failed to fetch remote commits:`] const failedFetch = [`Failed to fetch remote releases:`, `Failed to fetch remote commits:`]
const osList = ['macos', 'windows', 'linux'] const osList = ['macos', 'windows', 'linux']
const macExtensionList = ['.app', '.dmg'] const macExtensionList = ['.dmg', '.pkg']
const windowsExtensionList = ['.exe', '.msi'] const windowsExtensionList = ['.exe', '.msi']
const blacklistPrefixes = [ const blacklistPrefixes = [
+6
View File
@@ -10,11 +10,17 @@ export async function getOS() {
return await invoke('plugin:utils|get_os') return await invoke('plugin:utils|get_os')
} }
// [AR] Feature
export async function getArtifact(downloadurl, filename, ostype, autoupdatesupported) { export async function getArtifact(downloadurl, filename, ostype, autoupdatesupported) {
console.log('Downloading build', downloadurl, filename, ostype, autoupdatesupported) console.log('Downloading build', downloadurl, filename, ostype, autoupdatesupported)
return await invoke('plugin:utils|get_artifact', { downloadurl, filename, ostype, autoupdatesupported }) return await invoke('plugin:utils|get_artifact', { downloadurl, filename, ostype, autoupdatesupported })
} }
// [AR] Patch fix
export async function applyMigrationFix(eol) {
return await invoke('plugin:utils|apply_migration_fix', { eol })
}
export async function openPath(path) { export async function openPath(path) {
return await invoke('plugin:utils|open_path', { path }) return await invoke('plugin:utils|open_path', { path })
} }
+2 -2
View File
@@ -38,7 +38,7 @@ import {
import { get as getSettings } from '@/helpers/settings.ts' import { get as getSettings } from '@/helpers/settings.ts'
import { get_default_user, login as login_flow, users } from '@/helpers/auth' import { get_default_user, login as login_flow, users } from '@/helpers/auth'
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts' import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
import { generateSkinPreviews, map } from '@/helpers/rendering/batch-skin-renderer.ts' import { generateSkinPreviews, skinBlobUrlMap } from '@/helpers/rendering/batch-skin-renderer.ts'
import { handleSevereError } from '@/store/error' import { handleSevereError } from '@/store/error'
import { trackEvent } from '@/helpers/analytics' import { trackEvent } from '@/helpers/analytics'
import type AccountsCard from '@/components/ui/AccountsCard.vue' import type AccountsCard from '@/components/ui/AccountsCard.vue'
@@ -215,7 +215,7 @@ async function loadCurrentUser() {
function getBakedSkinTextures(skin: Skin): RenderResult | undefined { function getBakedSkinTextures(skin: Skin): RenderResult | undefined {
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}` const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
return map.get(key) return skinBlobUrlMap.get(key)
} }
async function login() { async function login() {
@@ -483,7 +483,7 @@ onUnmounted(() => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
height: calc(100vh - 11rem); height: 100vh;
} }
.button-row { .button-row {
+1
View File
@@ -218,6 +218,7 @@ fn main() {
"utils", "utils",
InlinedPlugin::new() InlinedPlugin::new()
.commands(&[ .commands(&[
"apply_migration_fix",
"get_artifact", "get_artifact",
"get_os", "get_os",
"should_disable_mouseover", "should_disable_mouseover",
+11 -1
View File
@@ -11,10 +11,12 @@ use dashmap::DashMap;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use theseus::prelude::canonicalize; use theseus::prelude::canonicalize;
use url::Url; use url::Url;
use theseus::util::utils;
pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> { pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
tauri::plugin::Builder::new("utils") tauri::plugin::Builder::new("utils")
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
apply_migration_fix,
get_artifact, get_artifact,
get_os, get_os,
should_disable_mouseover, should_disable_mouseover,
@@ -27,9 +29,17 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
.build() .build()
} }
/// [AR] Patch fix
#[tauri::command]
pub async fn apply_migration_fix(eol: &str) -> Result<bool> {
let result = utils::apply_migration_fix(eol).await?;
Ok(result)
}
/// [AR] Feature
#[tauri::command] #[tauri::command]
pub async fn get_artifact(downloadurl: &str, filename: &str, ostype: &str, autoupdatesupported: bool) -> Result<()> { pub async fn get_artifact(downloadurl: &str, filename: &str, ostype: &str, autoupdatesupported: bool) -> Result<()> {
theseus::download::init_download(downloadurl, filename, ostype, autoupdatesupported).await; let _ = utils::init_download(downloadurl, filename, ostype, autoupdatesupported).await;
Ok(()) Ok(())
} }
+1 -1
View File
@@ -157,7 +157,7 @@ fn main() {
*/ */
let _log_guard = theseus::start_logger(); let _log_guard = theseus::start_logger();
tracing::info!("Initialized tracing subscriber. Loading Modrinth App!"); tracing::info!("Initialized tracing subscriber. Loading AstralRinth App!");
let mut builder = tauri::Builder::default(); let mut builder = tauri::Builder::default();
+1 -1
View File
@@ -41,7 +41,7 @@
] ]
}, },
"productName": "AstralRinth App", "productName": "AstralRinth App",
"version": "0.10.302", "version": "0.10.303",
"mainBinaryName": "AstralRinth App", "mainBinaryName": "AstralRinth App",
"identifier": "AstralRinthApp", "identifier": "AstralRinthApp",
"plugins": { "plugins": {
-55
View File
@@ -1,55 +0,0 @@
use std::process::exit;
use reqwest;
use tokio::fs::File as AsyncFile;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
async fn download_file(download_url: &str, local_filename: &str, os_type: &str, auto_update_supported: bool) -> Result<(), Box<dyn std::error::Error>> {
let download_dir = dirs::download_dir().ok_or("[download_file] • Failed to determine download directory")?;
let full_path = download_dir.join(local_filename);
let response = reqwest::get(download_url).await?;
let bytes = response.bytes().await?;
let mut dest_file = AsyncFile::create(&full_path).await?;
dest_file.write_all(&bytes).await?;
println!("[download_file] • File downloaded to: {:?}", full_path);
if auto_update_supported {
let status;
if os_type.to_lowercase() == "Windows".to_lowercase() {
status = Command::new("explorer")
.arg(download_dir.display().to_string())
.status()
.await
.expect("[download_file] • Failed to open downloads folder");
} else if os_type.to_lowercase() == "MacOS".to_lowercase() {
status = Command::new("open")
.arg(full_path.to_str().unwrap_or_default())
.status()
.await
.expect("[download_file] • Failed to execute command");
} else {
status = Command::new(".")
.arg(full_path.to_str().unwrap_or_default())
.status()
.await
.expect("[download_file] • Failed to execute command");
}
if status.success() {
println!("[download_file] • File opened successfully!");
} else {
eprintln!("[download_file] • Failed to open the file. Exit code: {:?}", status.code());
}
}
Ok(())
}
pub async fn init_download(download_url: &str, local_filename: &str, os_type: &str, auto_update_supported: bool) {
println!("[init_download] • Initialize downloading from • {:?}", download_url);
println!("[init_download] • Save local file name • {:?}", local_filename);
if let Err(e) = download_file(download_url, local_filename, os_type, auto_update_supported).await {
eprintln!("[init_download] • An error occurred! Failed to download the file: {}", e);
} else {
println!("[init_download] • Code finishes without errors.");
exit(0)
}
}
+1 -1
View File
@@ -12,8 +12,8 @@ pub mod pack;
pub mod process; pub mod process;
pub mod profile; pub mod profile;
pub mod settings; pub mod settings;
pub mod update; // [AR] Feature
pub mod tags; pub mod tags;
pub mod download; // AstralRinth
pub mod worlds; pub mod worlds;
pub mod data { pub mod data {
+117
View File
@@ -0,0 +1,117 @@
use reqwest;
use std::path::PathBuf;
use tokio::fs::File as AsyncFile;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
pub(crate) async fn get_resource(
download_url: &str,
local_filename: &str,
os_type: &str,
auto_update_supported: bool,
) -> Result<(), Box<dyn std::error::Error>> {
let download_dir = dirs::download_dir()
.ok_or("[AR] • Failed to determine download directory")?;
let full_path = download_dir.join(local_filename);
let response = reqwest::get(download_url).await?;
let bytes = response.bytes().await?;
let mut dest_file = AsyncFile::create(&full_path).await?;
dest_file.write_all(&bytes).await?;
tracing::info!("[AR] • File downloaded to: {:?}", full_path);
if auto_update_supported {
let result = match os_type.to_lowercase().as_str() {
"windows" => handle_windows_file(&full_path).await,
"macos" => open_macos_file(&full_path).await,
_ => open_default(&full_path).await,
};
match result {
Ok(_) => tracing::info!("[AR] • File opened successfully!"),
Err(e) => tracing::info!("[AR] • Failed to open file: {e}"),
}
}
Ok(())
}
async fn handle_windows_file(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let filename = path
.file_name()
.and_then(|f| f.to_str())
.unwrap_or_default()
.to_lowercase();
if filename.ends_with(".exe") || filename.ends_with(".msi") {
tracing::info!("[AR] • Detected installer: {}", filename);
run_windows_installer(path).await
} else {
open_windows_folder(path).await
}
}
async fn run_windows_installer(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let installer_path = path.to_str().unwrap_or_default();
let status = if installer_path.ends_with(".msi") {
Command::new("msiexec")
.args(&["/i", installer_path, "/quiet"])
.status()
.await?
} else {
Command::new("cmd")
.args(&["/C", installer_path])
.status()
.await?
};
if status.success() {
tracing::info!("[AR] • Installer started successfully.");
Ok(())
} else {
tracing::error!("Installer failed. Exit code: {:?}", status.code());
tracing::info!("[AR] • Trying to open folder...");
open_windows_folder(path).await
}
}
async fn open_windows_folder(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let folder = path.parent().unwrap_or(path);
let status = Command::new("explorer")
.arg(folder.display().to_string())
.status()
.await?;
if !status.success() {
Err(format!("Exit code: {:?}", status.code()).into())
} else {
Ok(())
}
}
async fn open_macos_file(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let status = Command::new("open")
.arg(path.to_str().unwrap_or_default())
.status()
.await?;
if !status.success() {
Err(format!("Exit code: {:?}", status.code()).into())
} else {
Ok(())
}
}
async fn open_default(path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
let status = Command::new(".")
.arg(path.to_str().unwrap_or_default())
.status()
.await?;
if !status.success() {
Err(format!("Exit code: {:?}", status.code()).into())
} else {
Ok(())
}
}
+3 -2
View File
@@ -14,7 +14,7 @@ use chrono::Utc;
use daedalus as d; use daedalus as d;
use daedalus::minecraft::{LoggingSide, RuleAction, VersionInfo}; use daedalus::minecraft::{LoggingSide, RuleAction, VersionInfo};
use daedalus::modded::LoaderVersion; use daedalus::modded::LoaderVersion;
use rand::seq::SliceRandom; // AstralRinth use rand::seq::SliceRandom; // [AR] Feature
use regex::Regex; use regex::Regex;
use serde::Deserialize; use serde::Deserialize;
use st::Profile; use st::Profile;
@@ -633,7 +633,7 @@ pub async fn launch_minecraft(
command.arg("--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED"); command.arg("--add-opens=jdk.internal/jdk.internal.misc=ALL-UNNAMED");
} }
// Patched by AstralRinth // [AR] Patch
if credentials.access_token == "null" && credentials.refresh_token == "null" { if credentials.access_token == "null" && credentials.refresh_token == "null" {
if version_jar == "1.16.4" || version_jar == "1.16.5" { if version_jar == "1.16.4" || version_jar == "1.16.5" {
let invalid_url = "https://invalid.invalid"; let invalid_url = "https://invalid.invalid";
@@ -743,6 +743,7 @@ pub async fn launch_minecraft(
} }
} }
// [AR] Feature
let selected_phrase = ACTIVE_STATE.choose(&mut rand::thread_rng()).unwrap(); let selected_phrase = ACTIVE_STATE.choose(&mut rand::thread_rng()).unwrap();
let _ = state let _ = state
.discord_rpc .discord_rpc
+1 -1
View File
@@ -8,7 +8,7 @@ and launching Modrinth mod packs
#![deny(unused_must_use)] #![deny(unused_must_use)]
#[macro_use] #[macro_use]
mod util; pub mod util; // [AR] Refactor
mod api; mod api;
mod config; mod config;
+107 -79
View File
@@ -1,18 +1,32 @@
use crate::ErrorKind;
use crate::state::DirectoryInfo; use crate::state::DirectoryInfo;
use sqlx::sqlite::{ use sqlx::sqlite::{
SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions,
}; };
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
use std::env; use std::collections::HashMap;
use tokio::time::Instant;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use tokio::time::Instant;
pub(crate) async fn connect() -> crate::Result<Pool<Sqlite>> { pub(crate) async fn connect() -> crate::Result<Pool<Sqlite>> {
let pool = connect_without_migrate().await?;
sqlx::migrate!().run(&pool).await?;
if let Err(err) = stale_data_cleanup(&pool).await {
tracing::warn!(
"Failed to clean up stale data from state database: {err}"
);
}
Ok(pool)
}
// [AR] Feature. Implement SQLite3 connection without SQLx migrations.
async fn connect_without_migrate() -> crate::Result<Pool<Sqlite>> {
let settings_dir = DirectoryInfo::get_initial_settings_dir().ok_or( let settings_dir = DirectoryInfo::get_initial_settings_dir().ok_or(
crate::ErrorKind::FSError( ErrorKind::FSError("Could not find valid config dir".to_string()),
"Could not find valid config dir".to_string(),
),
)?; )?;
if !settings_dir.exists() { if !settings_dir.exists() {
@@ -20,7 +34,6 @@ pub(crate) async fn connect() -> crate::Result<Pool<Sqlite>> {
} }
let db_path = settings_dir.join("app.db"); let db_path = settings_dir.join("app.db");
let db_exists = db_path.exists();
let uri = format!("sqlite:{}", db_path.display()); let uri = format!("sqlite:{}", db_path.display());
let conn_options = SqliteConnectOptions::from_str(&uri)? let conn_options = SqliteConnectOptions::from_str(&uri)?
@@ -34,22 +47,6 @@ pub(crate) async fn connect() -> crate::Result<Pool<Sqlite>> {
.connect_with(conn_options) .connect_with(conn_options)
.await?; .await?;
if db_exists {
fix_modrinth_issued_migrations(&pool).await?;
}
sqlx::migrate!().run(&pool).await?;
if !db_exists {
fix_modrinth_issued_migrations(&pool).await?;
}
if let Err(err) = stale_data_cleanup(&pool).await {
tracing::warn!(
"Failed to clean up stale data from state database: {err}"
);
}
Ok(pool) Ok(pool)
} }
@@ -75,72 +72,103 @@ async fn stale_data_cleanup(pool: &Pool<Sqlite>) -> crate::Result<()> {
Ok(()) Ok(())
} }
/* /*
// Patch by AstralRinth - 08.07.2025 // [AR] Patch fix
Problem files: Problem files, view detailed information in .gitattributes:
/packages/app-lib/migrations/20240711194701_init.sql !eol /packages/app-lib/migrations/20240711194701_init.sql !eol
CRLF -> 4c47e326f16f2b1efca548076ce638d4c90dd610172fe48c47d6de9bc46ef1c5abeadfdea05041ddd72c3819fa10c040
LF -> e973512979feac07e415405291eefafc1ef0bd89454958ad66f5452c381db8679c20ffadab55194ecf6ba8ec4ca2db21
/packages/app-lib/migrations/20240813205023_drop-active-unique.sql !eol /packages/app-lib/migrations/20240813205023_drop-active-unique.sql !eol
CRLF -> C8FD2EFE72E66E394732599EA8D93CE1ED337F098697B3ADAD40DD37CC6367893E199A8D7113B44A3D0FFB537692F91D
LF -> 5b53534a7ffd74eebede234222be47e1d37bd0cc5fee4475212491b0c0379c16e3079e08eee0af959b1fa20835eeb206
/packages/app-lib/migrations/20240930001852_disable-personalized-ads.sql !eol /packages/app-lib/migrations/20240930001852_disable-personalized-ads.sql !eol
CRLF -> C0DE804F171B5530010EDAE087A6E75645C0E90177E28365F935C9FDD9A5C68E24850B8C1498E386A379D525D520BC57
LF -> c0de804f171b5530010edae087a6e75645c0e90177e28365f935c9fdd9a5c68e24850b8c1498e386a379d525d520bc57
/packages/app-lib/migrations/20241222013857_feature-flags.sql !eol /packages/app-lib/migrations/20241222013857_feature-flags.sql !eol
CRLF -> 6B6F097E5BB45A397C96C3F1DC9C2A18433564E81DB264FE08A4775198CCEAC03C9E63C3605994ECB19C281C37D8F6AE
LF -> c17542cb989a0466153e695bfa4717f8970feee185ca186a2caa1f2f6c5d4adb990ab97c26cacfbbe09c39ac81551704
*/ */
async fn fix_modrinth_issued_migrations( pub(crate) async fn apply_migration_fix(eol: &str) -> crate::Result<bool> {
pool: &Pool<Sqlite>, let started = Instant::now();
) -> crate::Result<()> {
let arch = env::consts::ARCH;
let os = env::consts::OS;
tracing::info!("Running on OS: {}, ARCH: {}", os, arch); // Create connection to the database without migrations
let pool = connect_without_migrate().await?;
tracing::info!(
"⚙️ Patching Modrinth corrupted migration checksums using EOL standard: {eol}"
);
if os == "windows" && arch == "x86_64" { // validate EOL input
tracing::warn!("🛑 Skipping migration checksum fix on Windows x86_64 (runtime-detected)"); if eol != "lf" && eol != "crlf" {
return Ok(()); return Ok(false);
}
// [eol][version] -> checksum
let checksums: HashMap<(&str, &str), &str> = HashMap::from([
(
("lf", "20240711194701"),
"e973512979feac07e415405291eefafc1ef0bd89454958ad66f5452c381db8679c20ffadab55194ecf6ba8ec4ca2db21",
),
(
("crlf", "20240711194701"),
"4c47e326f16f2b1efca548076ce638d4c90dd610172fe48c47d6de9bc46ef1c5abeadfdea05041ddd72c3819fa10c040",
),
(
("lf", "20240813205023"),
"5b53534a7ffd74eebede234222be47e1d37bd0cc5fee4475212491b0c0379c16e3079e08eee0af959b1fa20835eeb206",
),
(
("crlf", "20240813205023"),
"C8FD2EFE72E66E394732599EA8D93CE1ED337F098697B3ADAD40DD37CC6367893E199A8D7113B44A3D0FFB537692F91D",
),
(
("lf", "20240930001852"),
"c0de804f171b5530010edae087a6e75645c0e90177e28365f935c9fdd9a5c68e24850b8c1498e386a379d525d520bc57",
),
(
("crlf", "20240930001852"),
"C0DE804F171B5530010EDAE087A6E75645C0E90177E28365F935C9FDD9A5C68E24850B8C1498E386A379D525D520BC57",
),
(
("lf", "20241222013857"),
"c17542cb989a0466153e695bfa4717f8970feee185ca186a2caa1f2f6c5d4adb990ab97c26cacfbbe09c39ac81551704",
),
(
("crlf", "20241222013857"),
"6B6F097E5BB45A397C96C3F1DC9C2A18433564E81DB264FE08A4775198CCEAC03C9E63C3605994ECB19C281C37D8F6AE",
),
]);
let mut changed = false;
for ((eol_key, version), checksum) in checksums.iter() {
if *eol_key != eol {
continue;
} }
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!( tracing::info!(
"✅ Fixed all known Modrinth checksums for migrations in {:.2?}", "⏳ Patching checksum for migration {version} ({})",
elapsed eol.to_uppercase()
); );
Ok(())
let result = sqlx::query(&format!(
r#"
UPDATE "_sqlx_migrations"
SET checksum = X'{checksum}'
WHERE version = '{version}';
"#
))
.execute(&pool)
.await?;
if result.rows_affected() > 0 {
changed = true;
}
}
tracing::info!(
"✅ Checksum patching completed in {:.2?} (changes: {})",
started.elapsed(),
changed
);
Ok(changed)
} }
+1
View File
@@ -22,6 +22,7 @@ pub struct DirectoryInfo {
impl DirectoryInfo { impl DirectoryInfo {
// Get the settings directory // Get the settings directory
// init() is not needed for this function // init() is not needed for this function
// [AR] Patch fix. From PR.
pub fn get_initial_settings_dir() -> Option<PathBuf> { pub fn get_initial_settings_dir() -> Option<PathBuf> {
Self::env_path("THESEUS_CONFIG_DIR").or_else(|| { Self::env_path("THESEUS_CONFIG_DIR").or_else(|| {
if std::env::current_dir().ok()?.join("portable.txt").exists() { if std::env::current_dir().ok()?.join("portable.txt").exists() {
+5 -4
View File
@@ -1,16 +1,17 @@
// [AR] Feature
use std::{ use std::{
sync::{atomic::AtomicBool, Arc}, sync::{atomic::AtomicBool, Arc},
time::{SystemTime, UNIX_EPOCH}, // AstralRinth time::{SystemTime, UNIX_EPOCH},
}; };
use discord_rich_presence::{ use discord_rich_presence::{
activity::{Activity, Assets, Timestamps}, // AstralRinth activity::{Activity, Assets, Timestamps}, // [AR] Feature
DiscordIpc, DiscordIpcClient, DiscordIpc, DiscordIpcClient,
}; };
use rand::seq::SliceRandom; // AstralRinth use rand::seq::SliceRandom; // [AR] Feature
use tokio::sync::RwLock; use tokio::sync::RwLock;
use crate::util::utils; // AstralRinth use crate::util::utils; // [AR] Feature
use crate::State; use crate::State;
pub struct DiscordGuard { pub struct DiscordGuard {
+2 -2
View File
@@ -213,7 +213,7 @@ pub async fn login_finish(
Ok(credentials) Ok(credentials)
} }
// Patched by AstralRinth // [AR] Feature
#[tracing::instrument] #[tracing::instrument]
pub async fn offline_auth( pub async fn offline_auth(
name: &str, name: &str,
@@ -790,7 +790,7 @@ const MICROSOFT_CLIENT_ID: &str = "00000000402b5328";
const REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf"; const REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf";
const REQUESTED_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL"; const REQUESTED_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
/* AstralRinth /* [AR] Fix
* Weird visibility issue that didn't reproduce before * Weird visibility issue that didn't reproduce before
* Had to make DeviceToken and RequestWithDate pub(crate) to fix compilation error * Had to make DeviceToken and RequestWithDate pub(crate) to fix compilation error
*/ */
+1 -1
View File
@@ -3,5 +3,5 @@ pub mod fetch;
pub mod io; pub mod io;
pub mod jre; pub mod jre;
pub mod platform; pub mod platform;
pub mod utils; // AstralRinth pub mod utils; // [AR] Feature
pub mod server_ping; pub mod server_ping;
+46 -4
View File
@@ -1,16 +1,20 @@
///
/// [AR] Feature
///
use crate::Result;
use crate::api::update;
use crate::state::db;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::process;
use tokio::io; use tokio::io;
/*
AstralRinth Utils
*/
const PACKAGE_JSON_CONTENT: &str = const PACKAGE_JSON_CONTENT: &str =
// include_str!("../../../../apps/app-frontend/package.json"); // include_str!("../../../../apps/app-frontend/package.json");
include_str!("../../../../apps/app/tauri.conf.json"); include_str!("../../../../apps/app/tauri.conf.json");
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct Launcher { pub struct Launcher {
pub version: String pub version: String,
} }
pub fn read_package_json() -> io::Result<Launcher> { pub fn read_package_json() -> io::Result<Launcher> {
@@ -19,3 +23,41 @@ pub fn read_package_json() -> io::Result<Launcher> {
Ok(launcher) Ok(launcher)
} }
pub async fn apply_migration_fix(eol: &str) -> Result<bool> {
tracing::info!("[AR] • Attempting to apply migration fix");
let patched = db::apply_migration_fix(eol).await?;
if patched {
tracing::info!("[AR] • Successfully applied migration fix");
} else {
tracing::error!("[AR] • Failed to apply migration fix");
}
Ok(patched)
}
pub async fn init_download(
download_url: &str,
local_filename: &str,
os_type: &str,
auto_update_supported: bool,
) -> Result<()> {
println!("[AR] • Initialize downloading from • {:?}", download_url);
println!("[AR] • Save local file name • {:?}", local_filename);
if let Err(e) = update::get_resource(
download_url,
local_filename,
os_type,
auto_update_supported,
)
.await
{
eprintln!(
"[AR] • An error occurred! Failed to download the file: {}",
e
);
} else {
println!("[AR] • Code finishes without errors.");
process::exit(0)
}
Ok(())
}
+3 -3
View File
@@ -83,21 +83,21 @@ export const TwitterIcon = _TwitterIcon
export const WindowsIcon = _WindowsIcon export const WindowsIcon = _WindowsIcon
export const YouTubeIcon = _YouTubeIcon export const YouTubeIcon = _YouTubeIcon
// AstralRinth Icons // [AR] Feature. Icons
import _PirateIcon from './icons/pirate.svg?component' import _PirateIcon from './icons/pirate.svg?component'
import _MicrosoftIcon from './icons/microsoft.svg?component' import _MicrosoftIcon from './icons/microsoft.svg?component'
import _PirateShipIcon from './icons/pirate-ship.svg?component' import _PirateShipIcon from './icons/pirate-ship.svg?component'
import _AstralRinthLogo from './icons/astralrinth-logo.svg?component' import _AstralRinthLogo from './icons/astralrinth-logo.svg?component'
// AstralRinth Exports // [AR] Feature. Exports
export const PirateIcon = _PirateIcon export const PirateIcon = _PirateIcon
export const MicrosoftIcon = _MicrosoftIcon export const MicrosoftIcon = _MicrosoftIcon
export const PirateShipIcon = _PirateShipIcon export const PirateShipIcon = _PirateShipIcon
export const AstralRinthLogo = _AstralRinthLogo 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 ClassicPlayerModel } from './models/classic-player.gltf?url'
export { default as SlimPlayerModel } from './models/slim-player.gltf?url' export { default as SlimPlayerModel } from './models/slim-player.gltf?url'
-92
View File
@@ -1,92 +0,0 @@
{
"asset": { "version": "2.0", "generator": "Blockbench 4.12.4 glTF exporter" },
"scenes": [{ "nodes": [1], "name": "blockbench_export" }],
"scene": 0,
"nodes": [
{
"rotation": [0, 0, 0.19509032201612825, 0.9807852804032304],
"translation": [0.15625, 1, 0],
"name": "Cape",
"mesh": 0
},
{ "children": [0] }
],
"bufferViews": [
{ "buffer": 0, "byteOffset": 0, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 288, "byteLength": 288, "target": 34962, "byteStride": 12 },
{ "buffer": 0, "byteOffset": 576, "byteLength": 192, "target": 34962, "byteStride": 8 },
{ "buffer": 0, "byteOffset": 768, "byteLength": 72, "target": 34963 }
],
"buffers": [
{
"byteLength": 840,
"uri": "data:application/octet-stream;base64,AAAAPQAAAAAAAKA+AAAAPQAAAAAAAKC+AAAAPQAAgL8AAKA+AAAAPQAAgL8AAKC+AAAAvQAAAAAAAKC+AAAAvQAAAAAAAKA+AAAAvQAAgL8AAKC+AAAAvQAAgL8AAKA+AAAAvQAAAAAAAKC+AAAAPQAAAAAAAKC+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAvQAAgL8AAKA+AAAAPQAAgL8AAKA+AAAAvQAAgL8AAKC+AAAAPQAAgL8AAKC+AAAAvQAAAAAAAKA+AAAAPQAAAAAAAKA+AAAAvQAAgL8AAKA+AAAAPQAAgL8AAKA+AAAAPQAAAAAAAKC+AAAAvQAAAAAAAKC+AAAAPQAAgL8AAKC+AAAAvQAAgL8AAKC+AACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAPwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAACAvwAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgD8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAgL8AAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AAAAAAAAAAAAAIC/AACAPAAAgD0AADA+AACAPQAAgDwAAAg/AAAwPgAACD8AAEA+AACAPQAAsD4AAIA9AABAPgAACD8AALA+AAAIPwAAgDwAAAA9AACAPAAAgD0AADA+AAAAPQAAMD4AAIA9AAAwPgAAAD0AAKg+AAAAPQAAMD4AAAAAAACoPgAAAAAAAEA+AACAPQAAMD4AAIA9AABAPgAACD8AADA+AAAIPwAAAAAAAIA9AACAPAAAgD0AAAAAAAAIPwAAgDwAAAg/AAACAAEAAgADAAEABAAGAAUABgAHAAUACAAKAAkACgALAAkADAAOAA0ADgAPAA0AEAASABEAEgATABEAFAAWABUAFgAXABUA"
}
],
"accessors": [
{
"bufferView": 0,
"componentType": 5126,
"count": 24,
"max": [0.03125, 0, 0.3125],
"min": [-0.03125, -1, -0.3125],
"type": "VEC3"
},
{
"bufferView": 1,
"componentType": 5126,
"count": 24,
"max": [1, 1, 1],
"min": [-1, -1, -1],
"type": "VEC3"
},
{
"bufferView": 2,
"componentType": 5126,
"count": 24,
"max": [0.34375, 0.53125],
"min": [0, 0],
"type": "VEC2"
},
{
"bufferView": 3,
"componentType": 5123,
"count": 36,
"max": [23],
"min": [0],
"type": "SCALAR"
}
],
"materials": [
{
"pbrMetallicRoughness": {
"metallicFactor": 0,
"roughnessFactor": 1,
"baseColorTexture": { "index": 0 }
},
"alphaMode": "MASK",
"alphaCutoff": 0.05,
"doubleSided": true
}
],
"textures": [{ "sampler": 0, "source": 0, "name": "cape.png" }],
"samplers": [{ "magFilter": 9728, "minFilter": 9728, "wrapS": 33071, "wrapT": 33071 }],
"images": [
{
"mimeType": "image/png",
"uri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAAAXNSR0IArs4c6QAABCRJREFUaEPtlktoE1EUhm/sIzFpYsBgHyn2oRZt0C5ESxWFYutKNyKIWBBR6kpBd3Ur2J0FRdCiuKmIIG4qLnzgQqTUXSlVqdoXTU0lljRpYtOHI/8dznBnOm1mphMwJWeTzJ0795zznf/cex2MMXam2S/ht38siJ8V1lgd5mOBARerc3rZnmK37rwvCyk2nE6wezMRh+4EncH9B1ql+fkU64rPsaBz5bqh4T7Daxn1Kc5zVNeEePJIci0Aq71bzenY6JChwAnA0OBHQ/OtJLnWNwqAy61R1tO3k891ueRKoDKwtqbv7MGbgCnfRgHQoqG9hyX4hc/yiho+/HNqVPFtdj2jwTpQgd/RKT7/0ZUku/o4qAJw50KYXbzr4e+3BioYzc3kwGzAUCLWFw2+UBiCb3bNTDHiPQeAP3CGNmg/6VcSBpDu3hhvDQpOCwABwrQKsRIsqYAChy/EQAXwlPiZ3a3igNPkXIyTjr8o5L7PPdzGf59c+sV/faeWeIIIAHPJ8M3kc7l1K09LKghWAIgqoPZ7djPFTlxb4D6yAgBOQfntrUVWVeRk44tplXJorOVGkVIJTBCTpw9ECFYBIEkyMfmsAXh3u1qi5MlxbbGX/x1ZSCjBAAwgfPr6h49R5UVavk0FXC2wju5p07s6iqEF0PtqSlEf+bKzDRwdgaDU7AkoySJ5Oo/D6ZRq/H0yyuJ/lxkSheG/FgCNm7kL0BoEAG32sqtY1YIHd2/mGzTMVgD3y2slqjgW9xY6ma9AThAARIMiBtOpjADwTWc0bEkB+FZsSTxTW0KBsGPXx0yvrUpEeHAAQIM7wBJLcu8DwHaP/H8i6VSND6SiSjBlRW4WWUypVIBbIgzjVgB0tpdKqLReS0KVPTMTfH0ra2cEgAmAAEd+l1z52LybqwBQYAQAyZPh6gtDW4jjuC4fHx8wXSm0JDbeI95SRYHiFZlUaWVtPQiKAugl5E8ASAUYiy8vc0C474uGasPE5PHc4g0wK/f4obom6UNimol7kTZwQLANwOuqBokqDEeQf4lfvvnNxZJcBTBAGZplWQcQ3tcgwY9oWlXinRW4ugoAgNAWWe6ocn1QvgyRTUb4RZFVljlY/3hSBYCqb6cCZo8ekuATVRZPI/gQW8FWAI1VHganVgDQUajdA6y2gAgAcZFBjTBSpG0AcAqc3VVmCMDTbxGWZvIRCaMNkJ7pFMCzVQD4liCQ8kRFUlvaBkCvL+wYw2ZmNUgCgLajFhRPJlv3ADuS1VtjPQCk823S574ffN/x1dQqy8dHR5SN2Spcbaymz2mjwNYDAD4IQn3TDpVLQIAqNjwAANRrAdoI/3sAOF7Xe1khCNQGVH1bL0JGJb1R52VtD8gVYHkAuVKpbMWZV0C2yObKunkF5EqlshVnXgHZIpsr6/4DlbxcPydnT74AAAAASUVORK5CYII="
}
],
"meshes": [
{
"primitives": [
{
"mode": 4,
"attributes": { "POSITION": 0, "NORMAL": 1, "TEXCOORD_0": 2 },
"indices": 3,
"material": 0
}
]
}
]
}
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
+39
View File
@@ -0,0 +1,39 @@
// [AR] Feature
.neon-button.neon :deep(:is(button, a, .button-like)),
.neon-button.neon :slotted(:is(button, a, .button-like)),
.neon-button.neon :slotted(*) :is(button, a, .button-like) {
cursor: pointer;
background-color: transparent;
border: 1px solid #3e8cde;
color: #3e8cde;
text-shadow:
0 0 4px rgba(79, 173, 255, 0.5),
0 0 8px rgba(14, 98, 204, 0.5),
0 0 12px rgba(122, 31, 199, 0.5);
transition:
color 0.25s ease,
box-shadow 0.3s ease,
transform 0.15s ease;
box-shadow: 0 0 4px rgba(79, 173, 255, 0.5);
}
.bordered {
border-radius: 12px;
}
/* Hover */
.neon-button.neon
:deep(:is(button, a, .button-like):hover):not([disabled]):not(.disabled),
.neon-button.neon
:slotted(:is(button, a, .button-like):hover):not([disabled]):not(.disabled),
.neon-button.neon
:slotted(*) :is(button, a, .button-like):hover:not([disabled]):not(.disabled) {
color: #10fae5;
transform: scale(1.02);
box-shadow:
0 0 4px rgba(16, 250, 229, 0.3),
0 0 8px rgba(16, 250, 229, 0.2);
text-shadow:
0 0 2px rgba(16, 250, 229, 0.4),
0 0 4px rgba(16, 250, 229, 0.25);
}
+37
View File
@@ -0,0 +1,37 @@
// [AR] Feature
.neon-icon {
background-color: transparent;
color: #3e8cde;
text-shadow:
0 0 4px rgba(79, 173, 255, 0.5),
0 0 8px rgba(14, 98, 204, 0.5),
0 0 12px rgba(122, 31, 199, 0.5);
transition: transform 0.25s ease, color 0.25s ease, text-shadow 0.25s ease;
cursor: pointer;
display: inline-block;
}
/* Hover */
.neon-icon:hover {
color: #10fae5;
transform: scale(1.05);
text-shadow:
0 0 2px rgba(16, 250, 229, 0.4),
0 0 4px rgba(16, 250, 229, 0.25);
}
.neon-icon.pulse {
position: relative;
animation: neon-pulse 1s ease-in-out infinite;
filter: drop-shadow(0 0 6px #10fae5);
box-shadow: none;
}
@keyframes neon-pulse {
0%, 100% {
filter: drop-shadow(0 0 4px #10fae5);
}
50% {
filter: drop-shadow(0 0 12px #10fae5);
}
}
+28
View File
@@ -0,0 +1,28 @@
// [AR] Feature
.neon-text {
background-color: transparent;
color: #3e8cde;
text-shadow:
0 0 4px rgba(79, 173, 255, 0.5),
0 0 8px rgba(14, 98, 204, 0.5),
0 0 12px rgba(122, 31, 199, 0.5);
transition:
color 0.25s ease,
box-shadow 0.3s ease,
transform 0.15s ease;
white-space: normal;
word-wrap: break-word;
overflow-wrap: break-word;
max-width: 100%;
display: inline-block;
padding: 4px 8px;
}
/* Hover */
.neon-text:hover:not([disabled]):not(.disabled) {
color: #10fae5;
text-shadow:
0 0 2px rgba(16, 250, 229, 0.4),
0 0 4px rgba(16, 250, 229, 0.25);
}
+2
View File
@@ -32,6 +32,7 @@
"@modrinth/utils": "workspace:*", "@modrinth/utils": "workspace:*",
"@tresjs/cientos": "^4.3.0", "@tresjs/cientos": "^4.3.0",
"@tresjs/core": "^4.3.4", "@tresjs/core": "^4.3.4",
"@tresjs/post-processing": "^2.4.0",
"@types/markdown-it": "^14.1.1", "@types/markdown-it": "^14.1.1",
"@types/three": "^0.172.0", "@types/three": "^0.172.0",
"@vintl/how-ago": "^3.0.1", "@vintl/how-ago": "^3.0.1",
@@ -41,6 +42,7 @@
"floating-vue": "^5.2.2", "floating-vue": "^5.2.2",
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"markdown-it": "^13.0.2", "markdown-it": "^13.0.2",
"postprocessing": "^6.37.6",
"qrcode.vue": "^3.4.1", "qrcode.vue": "^3.4.1",
"three": "^0.172.0", "three": "^0.172.0",
"vue-multiselect": "3.0.0", "vue-multiselect": "3.0.0",
@@ -23,7 +23,7 @@
<TresCanvas <TresCanvas
shadows shadows
alpha alpha
:antialias="antialias" :antialias="true"
:renderer-options="{ :renderer-options="{
outputColorSpace: THREE.SRGBColorSpace, outputColorSpace: THREE.SRGBColorSpace,
toneMapping: THREE.NoToneMapping, toneMapping: THREE.NoToneMapping,
@@ -46,34 +46,37 @@
<primitive v-if="scene" :object="scene" /> <primitive v-if="scene" :object="scene" />
</Group> </Group>
<TresMesh <!-- <TresMesh
:position="[0, -0.095 * scale, 2]" :position="[0, -0.095 * scale, 2]"
:rotation="[-Math.PI / 2, 0, 0]" :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]" /> <TresCircleGeometry :args="[1, 128]" />
<TresMeshBasicMaterial <TresMeshBasicMaterial
color="#000000" color="#000000"
:opacity="0.2" :opacity="0.5"
transparent transparent
:depth-write="false" :depth-write="false"
/> />
</TresMesh> </TresMesh> -->
</Group>
</Suspense>
<Suspense>
<EffectComposerPmndrs>
<FXAAPmndrs />
</EffectComposerPmndrs>
</Suspense>
<Suspense>
<TresMesh <TresMesh
:position="[0, -0.1 * scale, 2]" :position="[0, -0.1 * scale, 2]"
:rotation="[-Math.PI / 2, 0, 0]" :rotation="[-Math.PI / 2, 0, 0]"
:scale="[0.75 * scale, 0.75 * scale, 0.75 * scale]" :scale="[0.75 * scale, 0.75 * scale, 0.75 * scale]"
> >
<TresCircleGeometry :args="[1, 128]" /> <TresCircleGeometry :args="[1, 128]" />
<TresMeshBasicMaterial <TresShaderMaterial v-bind="radialSpotlightShader" />
:map="radialTexture"
transparent
:depth-write="false"
:blending="THREE.AdditiveBlending"
/>
</TresMesh> </TresMesh>
</Group>
</Suspense> </Suspense>
<TresPerspectiveCamera <TresPerspectiveCamera
@@ -101,6 +104,7 @@
import * as THREE from 'three' import * as THREE from 'three'
import { useGLTF } from '@tresjs/cientos' import { useGLTF } from '@tresjs/cientos'
import { useTexture, TresCanvas, useRenderLoop } from '@tresjs/core' import { useTexture, TresCanvas, useRenderLoop } from '@tresjs/core'
import { EffectComposerPmndrs, FXAAPmndrs } from '@tresjs/post-processing'
import { import {
shallowRef, shallowRef,
ref, ref,
@@ -115,13 +119,11 @@ import {
import { import {
applyTexture, applyTexture,
applyCapeTexture, applyCapeTexture,
attachCapeToBody,
findBodyNode,
createTransparentTexture, createTransparentTexture,
loadTexture as loadSkinTexture, loadTexture as loadSkinTexture,
} from '@modrinth/utils' } from '@modrinth/utils'
import { useDynamicFontSize } from '../../composables' import { useDynamicFontSize } from '../../composables'
import { CapeModel, ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets' import { ClassicPlayerModel, SlimPlayerModel } from '@modrinth/assets'
interface AnimationConfig { interface AnimationConfig {
baseAnimation: string baseAnimation: string
@@ -136,7 +138,6 @@ const props = withDefaults(
capeSrc?: string capeSrc?: string
variant?: 'SLIM' | 'CLASSIC' | 'UNKNOWN' variant?: 'SLIM' | 'CLASSIC' | 'UNKNOWN'
nametag?: string nametag?: string
antialias?: boolean
scale?: number scale?: number
fov?: number fov?: number
initialRotation?: number initialRotation?: number
@@ -144,7 +145,6 @@ const props = withDefaults(
}>(), }>(),
{ {
variant: 'CLASSIC', variant: 'CLASSIC',
antialias: false,
scale: 1, scale: 1,
fov: 40, fov: 40,
capeSrc: undefined, capeSrc: undefined,
@@ -177,9 +177,6 @@ const selectedModelSrc = computed(() =>
) )
const scene = shallowRef<THREE.Object3D | null>(null) 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 lastCapeSrc = ref<string | undefined>(undefined)
const texture = shallowRef<THREE.Texture | null>(null) const texture = shallowRef<THREE.Texture | null>(null)
const capeTexture = 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 randomAnimationTimer = ref<number | null>(null)
const lastRandomAnimation = ref<string>('') 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) const { baseAnimation, randomAnimations } = toRefs(props.animationConfig)
function initializeAnimations(loadedScene: THREE.Object3D, clips: THREE.AnimationClip[]) { function initializeAnimations(loadedScene: THREE.Object3D, clips: THREE.AnimationClip[]) {
@@ -400,11 +445,9 @@ async function loadModel(src: string) {
if (texture.value) { if (texture.value) {
applyTexture(scene.value, texture.value) applyTexture(scene.value, texture.value)
texture.value.needsUpdate = true
} }
bodyNode.value = findBodyNode(loadedScene) applyCapeTexture(scene.value, capeTexture.value, transparentTexture)
capeAttached.value = false
if (animations && animations.length > 0) { if (animations && animations.length > 0) {
initializeAnimations(loadedScene, animations) 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) { async function loadAndApplyTexture(src: string) {
if (!src) return null if (!src) return null
@@ -465,25 +492,9 @@ async function loadAndApplyCapeTexture(src: string | undefined) {
capeTexture.value = null capeTexture.value = null
} }
if (capeScene.value) { if (scene.value) {
applyCapeTexture(capeScene.value, capeTexture.value, transparentTexture) 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]) const centre = ref<[number, number, number]>([0, 1, 0])
@@ -539,39 +550,6 @@ function onCanvasClick() {
hasDragged.value = false 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(selectedModelSrc, (src) => loadModel(src))
watch( watch(
() => props.textureSrc, () => props.textureSrc,
@@ -587,7 +565,6 @@ watch(
watch( watch(
() => props.capeSrc, () => props.capeSrc,
async (newCapeSrc) => { async (newCapeSrc) => {
await loadCape()
await loadAndApplyCapeTexture(newCapeSrc) await loadAndApplyCapeTexture(newCapeSrc)
}, },
) )
@@ -619,8 +596,6 @@ onBeforeMount(async () => {
if (props.capeSrc) { if (props.capeSrc) {
await loadAndApplyCapeTexture(props.capeSrc) await loadAndApplyCapeTexture(props.capeSrc)
} }
await loadCape()
} catch (error) { } catch (error) {
console.error('Failed to initialize skin preview:', error) console.error('Failed to initialize skin preview:', error)
} }
+12 -42
View File
@@ -59,14 +59,11 @@ export function applyTexture(model: THREE.Object3D, texture: THREE.Texture): voi
model.traverse((child) => { model.traverse((child) => {
if ((child as THREE.Mesh).isMesh) { if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh const mesh = child as THREE.Mesh
// Skip cape meshes
if (mesh.name === 'Cape') return
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material] const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
materials.forEach((mat: THREE.Material) => { materials.forEach((mat: THREE.Material) => {
if (mat instanceof THREE.MeshStandardMaterial) { if (mat instanceof THREE.MeshStandardMaterial) {
if (mat.name !== 'cape') {
mat.map = texture mat.map = texture
mat.metalness = 0 mat.metalness = 0
mat.color.set(0xffffff) mat.color.set(0xffffff)
@@ -79,6 +76,7 @@ export function applyTexture(model: THREE.Object3D, texture: THREE.Texture): voi
mat.alphaTest = 0.1 mat.alphaTest = 0.1
mat.depthWrite = true mat.depthWrite = true
} }
}
}) })
} }
}) })
@@ -96,8 +94,9 @@ export function applyCapeTexture(
materials.forEach((mat: THREE.Material) => { materials.forEach((mat: THREE.Material) => {
if (mat instanceof THREE.MeshStandardMaterial) { if (mat instanceof THREE.MeshStandardMaterial) {
if (mat.name === 'cape') {
mat.map = texture || transparentTexture || null mat.map = texture || transparentTexture || null
mat.transparent = transparentTexture ? true : false mat.transparent = !texture || transparentTexture ? true : false
mat.metalness = 0 mat.metalness = 0
mat.color.set(0xffffff) mat.color.set(0xffffff)
mat.toneMapped = false mat.toneMapped = false
@@ -108,29 +107,14 @@ export function applyCapeTexture(
mat.depthWrite = true mat.depthWrite = true
mat.side = THREE.DoubleSide mat.side = THREE.DoubleSide
mat.alphaTest = 0.1 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 { export function findBodyNode(model: THREE.Object3D): THREE.Object3D | null {
let bodyNode: THREE.Object3D | null = null let bodyNode: THREE.Object3D | null = null
@@ -162,39 +146,25 @@ export function createTransparentTexture(): THREE.Texture {
export async function setupSkinModel( export async function setupSkinModel(
modelUrl: string, modelUrl: string,
textureUrl: string, textureUrl: string,
capeModelUrl?: string,
capeTextureUrl?: string, capeTextureUrl?: string,
config: SkinRendererConfig = {}, config: SkinRendererConfig = {},
): Promise<{ ): Promise<{
model: THREE.Object3D model: THREE.Object3D
bodyNode: THREE.Object3D | null 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 [gltf, texture] = await Promise.all([loadModel(modelUrl), loadTexture(textureUrl, config)])
const model = gltf.scene.clone() const model = gltf.scene.clone()
applyTexture(model, texture) applyTexture(model, texture)
if (capeTextureUrl) {
const capeTexture = await loadTexture(capeTextureUrl, config)
applyCapeTexture(model, capeTexture)
}
const bodyNode = findBodyNode(model) const bodyNode = findBodyNode(model)
let capeModel: THREE.Object3D | null = null
// Load cape if provided return { model, bodyNode }
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)
}
}
return { model, bodyNode, capeModel }
} }
export function disposeCaches(): void { export function disposeCaches(): void {
+32
View File
@@ -487,6 +487,9 @@ importers:
'@tresjs/core': '@tresjs/core':
specifier: ^4.3.4 specifier: ^4.3.4
version: 4.3.6(three@0.172.0)(typescript@5.5.4)(vue@3.5.13(typescript@5.5.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': '@types/markdown-it':
specifier: ^14.1.1 specifier: ^14.1.1
version: 14.1.1 version: 14.1.1
@@ -514,6 +517,9 @@ importers:
markdown-it: markdown-it:
specifier: ^13.0.2 specifier: ^13.0.2
version: 13.0.2 version: 13.0.2
postprocessing:
specifier: ^6.37.6
version: 6.37.6(three@0.172.0)
qrcode.vue: qrcode.vue:
specifier: ^3.4.1 specifier: ^3.4.1
version: 3.4.1(vue@3.5.13(typescript@5.5.4)) version: 3.4.1(vue@3.5.13(typescript@5.5.4))
@@ -2558,6 +2564,13 @@ packages:
three: '>=0.133' three: '>=0.133'
vue: '>=3.4' 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': '@trysound/sax@0.2.0':
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'} engines: {node: '>=10.13.0'}
@@ -6450,6 +6463,11 @@ packages:
posthog-js@1.158.2: posthog-js@1.158.2:
resolution: {integrity: sha512-ovb7GHHRNDf6vmuL+8lbDukewzDzQlLZXg3d475hrfHSBgidYeTxtLGtoBcUz4x6558BLDFjnSip+f3m4rV9LA==} 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: potpack@1.0.2:
resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==}
@@ -10455,6 +10473,16 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- typescript - 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': {} '@trysound/sax@0.2.0': {}
'@tweenjs/tween.js@23.1.3': {} '@tweenjs/tween.js@23.1.3': {}
@@ -15670,6 +15698,10 @@ snapshots:
preact: 10.23.2 preact: 10.23.2
web-vitals: 4.2.3 web-vitals: 4.2.3
postprocessing@6.37.6(three@0.172.0):
dependencies:
three: 0.172.0
potpack@1.0.2: {} potpack@1.0.2: {}
preact@10.23.2: {} preact@10.23.2: {}