fix: accounts card not having the right head
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
:class="{ expanded: mode === 'expanded', isolated: mode === 'isolated' }"
|
||||
>
|
||||
<div v-if="selectedAccount" class="selected account">
|
||||
<Avatar size="xs" :src="`https://mc-heads.net/avatar/${selectedAccount.profile.id}/128`" />
|
||||
<Avatar size="xs" :src="avatarUrl" />
|
||||
<div>
|
||||
<h4>{{ selectedAccount.profile.name }}</h4>
|
||||
<p>Selected</p>
|
||||
@@ -57,11 +57,7 @@
|
||||
<div v-for="account in displayAccounts" :key="account.profile.id" class="account-row">
|
||||
<Button class="option account" @click="setAccount(account)">
|
||||
<Avatar
|
||||
:src="
|
||||
account.profile.id == selectedAccount.profile.id
|
||||
? avatarUrl
|
||||
: `https://mc-heads.net/avatar/${account.profile.id}/128`
|
||||
"
|
||||
:src="getAccountAvatarUrl(account)"
|
||||
class="icon"
|
||||
/>
|
||||
<p>{{ account.profile.name }}</p>
|
||||
@@ -82,7 +78,7 @@
|
||||
<script setup>
|
||||
import { DropdownIcon, PlusIcon, TrashIcon, LogInIcon, SpinnerIcon } from '@modrinth/assets'
|
||||
import { Avatar, Button, Card } from '@modrinth/ui'
|
||||
import { ref, computed, onMounted, onBeforeUnmount, onUnmounted } from 'vue'
|
||||
import {ref, computed, onMounted, onBeforeUnmount, onUnmounted, watch} from 'vue'
|
||||
import {
|
||||
users,
|
||||
remove_user,
|
||||
@@ -95,6 +91,7 @@ import { trackEvent } from '@/helpers/analytics'
|
||||
import { process_listener } from '@/helpers/events'
|
||||
import { handleSevereError } from '@/store/error.js'
|
||||
import { get_available_skins } from '@/helpers/skins'
|
||||
import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
|
||||
defineProps({
|
||||
mode: {
|
||||
@@ -110,6 +107,7 @@ const accounts = ref({})
|
||||
const loginDisabled = ref(false)
|
||||
const defaultUser = ref()
|
||||
const equippedSkin = ref(null)
|
||||
const headUrlCache = ref(new Map())
|
||||
|
||||
async function refreshValues() {
|
||||
defaultUser.value = await get_default_user().catch(handleError)
|
||||
@@ -118,6 +116,15 @@ async function refreshValues() {
|
||||
try {
|
||||
const skins = await get_available_skins()
|
||||
equippedSkin.value = skins.find((skin) => skin.is_equipped)
|
||||
|
||||
if (equippedSkin.value) {
|
||||
try {
|
||||
const headUrl = await getPlayerHeadUrl(equippedSkin.value)
|
||||
headUrlCache.value.set(equippedSkin.value.texture_key, headUrl)
|
||||
} catch (error) {
|
||||
console.warn('Failed to get head render for equipped skin:', error)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
equippedSkin.value = null
|
||||
}
|
||||
@@ -140,11 +147,28 @@ const displayAccounts = computed(() =>
|
||||
|
||||
const avatarUrl = computed(() => {
|
||||
if (equippedSkin.value?.texture_key) {
|
||||
const cachedUrl = headUrlCache.value.get(equippedSkin.value.texture_key)
|
||||
if (cachedUrl) {
|
||||
return cachedUrl
|
||||
}
|
||||
return `https://mc-heads.net/avatar/${equippedSkin.value.texture_key}/128`
|
||||
}
|
||||
return `https://mc-heads.net/avatar/${selectedAccount.value.profile.id}/128`
|
||||
if (selectedAccount.value?.profile?.id) {
|
||||
return `https://mc-heads.net/avatar/${selectedAccount.value.profile.id}/128`
|
||||
}
|
||||
return 'https://launcher-files.modrinth.com/assets/steve_head.png'
|
||||
})
|
||||
|
||||
function getAccountAvatarUrl(account) {
|
||||
if (account.profile.id === selectedAccount.value?.profile?.id && equippedSkin.value?.texture_key) {
|
||||
const cachedUrl = headUrlCache.value.get(equippedSkin.value.texture_key)
|
||||
if (cachedUrl) {
|
||||
return cachedUrl
|
||||
}
|
||||
}
|
||||
return `https://mc-heads.net/avatar/${account.profile.id}/128`
|
||||
}
|
||||
|
||||
const selectedAccount = computed(() =>
|
||||
accounts.value.find((account) => account.profile.id === defaultUser.value),
|
||||
)
|
||||
|
||||
@@ -133,23 +133,167 @@ function getModelUrlForVariant(variant: string): string {
|
||||
}
|
||||
|
||||
export const map = reactive(new Map<string, RenderResult>())
|
||||
export const headMap = reactive(new Map<string, string>())
|
||||
const DEBUG_MODE = false
|
||||
|
||||
export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
||||
const validKeys = new Set<string>()
|
||||
const validHeadKeys = new Set<string>()
|
||||
|
||||
for (const skin of skins) {
|
||||
const key = `${skin.texture_key}+${skin.variant}+${skin.cape_id ?? 'no-cape'}`
|
||||
const headKey = `${skin.texture_key}-head`
|
||||
validKeys.add(key)
|
||||
validHeadKeys.add(headKey)
|
||||
}
|
||||
|
||||
try {
|
||||
await skinPreviewStorage.cleanupInvalidKeys(validKeys)
|
||||
await skinPreviewStorage.cleanupInvalidKeys(validHeadKeys)
|
||||
} catch (error) {
|
||||
console.warn('Failed to cleanup unused skin previews:', error)
|
||||
}
|
||||
}
|
||||
|
||||
export async function generatePlayerHeadBlob(
|
||||
skinUrl: string,
|
||||
size: number = 64
|
||||
): Promise<Blob> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
|
||||
img.onload = () => {
|
||||
try {
|
||||
const sourceCanvas = document.createElement('canvas');
|
||||
const sourceCtx = sourceCanvas.getContext('2d');
|
||||
|
||||
if (!sourceCtx) {
|
||||
throw new Error('Could not get 2D context from source canvas');
|
||||
}
|
||||
|
||||
sourceCanvas.width = img.width;
|
||||
sourceCanvas.height = img.height;
|
||||
|
||||
sourceCtx.drawImage(img, 0, 0);
|
||||
|
||||
const outputCanvas = document.createElement('canvas');
|
||||
const outputCtx = outputCanvas.getContext('2d');
|
||||
|
||||
if (!outputCtx) {
|
||||
throw new Error('Could not get 2D context from output canvas');
|
||||
}
|
||||
|
||||
outputCanvas.width = size;
|
||||
outputCanvas.height = size;
|
||||
|
||||
outputCtx.imageSmoothingEnabled = false;
|
||||
|
||||
const headImageData = sourceCtx.getImageData(8, 8, 8, 8);
|
||||
|
||||
const headCanvas = document.createElement('canvas');
|
||||
const headCtx = headCanvas.getContext('2d');
|
||||
|
||||
if (!headCtx) {
|
||||
throw new Error('Could not get 2D context from head canvas');
|
||||
}
|
||||
|
||||
headCanvas.width = 8;
|
||||
headCanvas.height = 8;
|
||||
headCtx.putImageData(headImageData, 0, 0);
|
||||
|
||||
outputCtx.drawImage(headCanvas, 0, 0, 8, 8, 0, 0, size, size);
|
||||
|
||||
const hatImageData = sourceCtx.getImageData(40, 8, 8, 8);
|
||||
|
||||
const hatCanvas = document.createElement('canvas');
|
||||
const hatCtx = hatCanvas.getContext('2d');
|
||||
|
||||
if (!hatCtx) {
|
||||
throw new Error('Could not get 2D context from hat canvas');
|
||||
}
|
||||
|
||||
hatCanvas.width = 8;
|
||||
hatCanvas.height = 8;
|
||||
hatCtx.putImageData(hatImageData, 0, 0);
|
||||
|
||||
const hatPixels = hatImageData.data;
|
||||
let hasHat = false;
|
||||
|
||||
for (let i = 3; i < hatPixels.length; i += 4) {
|
||||
if (hatPixels[i] > 0) {
|
||||
hasHat = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasHat) {
|
||||
outputCtx.drawImage(hatCanvas, 0, 0, 8, 8, 0, 0, size, size);
|
||||
}
|
||||
|
||||
outputCanvas.toBlob((blob) => {
|
||||
if (blob) {
|
||||
resolve(blob);
|
||||
} else {
|
||||
reject(new Error('Failed to create blob from canvas'));
|
||||
}
|
||||
}, 'image/png');
|
||||
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
reject(new Error('Failed to load skin texture image'));
|
||||
};
|
||||
|
||||
img.src = skinUrl;
|
||||
});
|
||||
}
|
||||
|
||||
async function generateHeadRender(skin: Skin): Promise<string> {
|
||||
const headKey = `${skin.texture_key}-head`
|
||||
|
||||
if (headMap.has(headKey)) {
|
||||
if (DEBUG_MODE) {
|
||||
const url = headMap.get(headKey)!
|
||||
URL.revokeObjectURL(url)
|
||||
headMap.delete(headKey)
|
||||
} else {
|
||||
return headMap.get(headKey)!
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const cached = await skinPreviewStorage.retrieve(headKey)
|
||||
if (cached && typeof cached === 'string') {
|
||||
headMap.set(headKey, cached)
|
||||
return cached
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to retrieve cached head render:', error)
|
||||
}
|
||||
|
||||
const skinUrl = await get_normalized_skin_texture(skin)
|
||||
const headBlob = await generatePlayerHeadBlob(skinUrl, 64)
|
||||
const headUrl = URL.createObjectURL(headBlob)
|
||||
|
||||
headMap.set(headKey, headUrl)
|
||||
|
||||
try {
|
||||
await skinPreviewStorage.store(headKey, headUrl)
|
||||
} catch (error) {
|
||||
console.warn('Failed to store head render in persistent storage:', error)
|
||||
}
|
||||
|
||||
return headUrl
|
||||
}
|
||||
|
||||
export async function getPlayerHeadUrl(skin: Skin): Promise<string> {
|
||||
return await generateHeadRender(skin)
|
||||
}
|
||||
|
||||
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
|
||||
const renderer = new BatchSkinRenderer()
|
||||
const capeModelUrl = '/src/assets/models/cape.gltf'
|
||||
@@ -203,6 +347,8 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
|
||||
} catch (error) {
|
||||
console.warn('Failed to store skin preview in persistent storage:', error)
|
||||
}
|
||||
|
||||
await generateHeadRender(skin)
|
||||
}
|
||||
} finally {
|
||||
renderer.dispose()
|
||||
|
||||
Reference in New Issue
Block a user