fix: 复用皮肤站逻辑并稳定玩家头像
Some checks failed
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Some checks failed
Axolotl desktop CI / guardrails (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
This commit is contained in:
@ -35,24 +35,44 @@
|
||||
/>
|
||||
</div>
|
||||
<div v-if="skinSitePlayers.length > 0" class="border-0 border-t border-solid border-surface-5">
|
||||
<div
|
||||
<button
|
||||
v-for="player in skinSitePlayers"
|
||||
:key="player.uuid"
|
||||
class="flex items-center gap-2 border-0 border-b border-solid border-surface-5 px-3 py-2 last:border-b-0"
|
||||
type="button"
|
||||
class="flex w-full cursor-pointer items-center gap-2 border-0 border-b border-solid border-surface-5 px-3 py-2 text-left transition-colors last:border-b-0 hover:bg-button-hover"
|
||||
:class="selectedSkinSitePlayerId === player.uuid ? 'bg-button-hover' : 'bg-transparent'"
|
||||
:aria-pressed="selectedSkinSitePlayerId === player.uuid"
|
||||
@click="setSkinSitePlayer(player.uuid)"
|
||||
>
|
||||
<Avatar :src="defaultSteveHeadUrl" size="24px" pixelated :unframed-natural-width="72" />
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<span class="truncate text-sm text-primary">{{ player.name }}</span>
|
||||
<span class="truncate text-xs text-secondary">{{ player.uuid }}</span>
|
||||
</div>
|
||||
<span class="shrink-0 text-xs text-secondary">
|
||||
{{
|
||||
launcherAccountIds.has(player.uuid)
|
||||
? formatMessage(messages.skinSitePlayerReady)
|
||||
: formatMessage(messages.skinSitePlayerSynced)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<Avatar
|
||||
v-if="player.skinState === 'ready' && player.headDataUrl"
|
||||
:src="player.headDataUrl"
|
||||
size="32px"
|
||||
pixelated
|
||||
:unframed-natural-width="36"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="h-8 w-8 shrink-0 rounded-sm border-2 border-dashed border-secondary/45 bg-surface-5/50 text-secondary"
|
||||
style="
|
||||
background-image:
|
||||
linear-gradient(currentColor, currentColor),
|
||||
linear-gradient(currentColor, currentColor),
|
||||
linear-gradient(currentColor, currentColor);
|
||||
background-position:
|
||||
6px 8px,
|
||||
22px 8px,
|
||||
8px 20px;
|
||||
background-repeat: no-repeat;
|
||||
background-size:
|
||||
4px 4px,
|
||||
4px 4px,
|
||||
16px 2px;
|
||||
"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span class="min-w-0 flex-1 truncate text-sm text-primary">{{ player.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-else-if="skinSitePlayersStatus === 'ready'" class="m-0 px-3 pb-3 text-sm text-secondary">
|
||||
{{ formatMessage(messages.noSkinSitePlayers) }}
|
||||
@ -250,11 +270,12 @@ import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import axolotlLogo from '@/assets/netherstar.png'
|
||||
import steveSkinTexture from '@/assets/skins/steve.png?inline'
|
||||
import MinecraftLoginModal from '@/components/ui/MinecraftLoginModal.vue'
|
||||
import {
|
||||
openSkinSiteLogin,
|
||||
requestSkinSitePlayers,
|
||||
selectedSkinSitePlayerId,
|
||||
selectSkinSitePlayer,
|
||||
skinSitePlayers,
|
||||
skinSitePlayersStatus,
|
||||
skinSiteStatus,
|
||||
@ -347,25 +368,14 @@ let refreshGeneration = 0
|
||||
let headRefreshTimer: ReturnType<typeof setTimeout> | undefined
|
||||
let defaultUserUpdateQueue = Promise.resolve()
|
||||
const minecraftLoginModal = ref<InstanceType<typeof MinecraftLoginModal> | null>(null)
|
||||
const launcherAccountIds = computed(() => new Set(accounts.value.map((account) => account.profile.id)))
|
||||
|
||||
async function refreshSkinSitePlayers() {
|
||||
await requestSkinSitePlayers().catch(() => {})
|
||||
}
|
||||
|
||||
function createSkinHeadDataUrl(textureUrl: string) {
|
||||
const escapedTextureUrl = textureUrl
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 8 8" shape-rendering="crispEdges"><image href="${escapedTextureUrl}" x="-8" y="-8" width="64" height="64" style="image-rendering:pixelated"/><image href="${escapedTextureUrl}" x="-40" y="-8" width="64" height="64" style="image-rendering:pixelated"/></svg>`
|
||||
|
||||
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`
|
||||
function setSkinSitePlayer(playerId: string) {
|
||||
selectSkinSitePlayer(playerId)
|
||||
}
|
||||
|
||||
const defaultSteveHeadUrl = createSkinHeadDataUrl(steveSkinTexture)
|
||||
|
||||
const HEAD_REFRESH_RETRY_DELAYS = [1500, 5000, 15000, 30000] as const
|
||||
const HEAD_REFRESH_CONTINUOUS_DELAY = 60_000
|
||||
|
||||
@ -609,6 +619,7 @@ function persistDefaultUser(userId: string) {
|
||||
async function setAccount(account: MinecraftCredential) {
|
||||
const userId = account.profile.id
|
||||
refreshGeneration += 1
|
||||
selectSkinSitePlayer(null)
|
||||
defaultUser.value = userId
|
||||
equippedSkin.value = null
|
||||
|
||||
@ -618,6 +629,16 @@ async function setAccount(account: MinecraftCredential) {
|
||||
if (defaultUser.value === userId) notifyAccountChange()
|
||||
}
|
||||
|
||||
watch(
|
||||
[skinSitePlayers, defaultUser],
|
||||
([availablePlayers, selectedLocalUser]) => {
|
||||
if (!selectedLocalUser && !selectedSkinSitePlayerId.value && availablePlayers.length > 0) {
|
||||
selectSkinSitePlayer(availablePlayers[0].uuid)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
async function login() {
|
||||
if (offline.value) return
|
||||
loginDisabled.value = true
|
||||
@ -708,14 +729,6 @@ const messages = defineMessages({
|
||||
id: 'minecraft-account.skin-site.players.error',
|
||||
defaultMessage: 'Could not refresh the player list. Previously loaded players are kept.',
|
||||
},
|
||||
skinSitePlayerReady: {
|
||||
id: 'minecraft-account.skin-site.player.ready',
|
||||
defaultMessage: 'Ready to launch',
|
||||
},
|
||||
skinSitePlayerSynced: {
|
||||
id: 'minecraft-account.skin-site.player.synced',
|
||||
defaultMessage: 'Synced from skin site',
|
||||
},
|
||||
offlineMode: {
|
||||
id: 'minecraft-account.offline-mode',
|
||||
defaultMessage: 'Offline mode',
|
||||
|
||||
@ -1,14 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { defineMessages, useVIntl } from '@modrinth/ui'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
receiveSkinSiteMessage,
|
||||
resetSkinSiteSession,
|
||||
setSkinSiteFrame,
|
||||
SKIN_SITE_ORIGIN,
|
||||
skinSiteFrameUrl,
|
||||
} from '@/composables/skin-site-session'
|
||||
import { skinSiteFrameUrl } from '@/composables/skin-site-session'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const messages = defineMessages({
|
||||
@ -18,42 +11,12 @@ const messages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
const frame = ref<HTMLIFrameElement>()
|
||||
let lastMessage = 0
|
||||
let expiryTimer: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
function connect() {
|
||||
const contentWindow = frame.value?.contentWindow ?? null
|
||||
setSkinSiteFrame(contentWindow)
|
||||
contentWindow?.postMessage({ type: 'starlight-skin-session-connect' }, SKIN_SITE_ORIGIN)
|
||||
}
|
||||
|
||||
function receive(event: MessageEvent) {
|
||||
if (receiveSkinSiteMessage(event, frame.value?.contentWindow ?? null)) lastMessage = Date.now()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', receive)
|
||||
connect()
|
||||
expiryTimer = setInterval(() => {
|
||||
if (lastMessage && Date.now() - lastMessage > 90_000) resetSkinSiteSession()
|
||||
}, 10_000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('message', receive)
|
||||
clearInterval(expiryTimer)
|
||||
setSkinSiteFrame(null)
|
||||
resetSkinSiteSession()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<iframe
|
||||
ref="frame"
|
||||
:src="skinSiteFrameUrl"
|
||||
:title="formatMessage(messages.frameTitle)"
|
||||
class="block h-full min-h-0 w-full border-0"
|
||||
@load="connect"
|
||||
/>
|
||||
</template>
|
||||
|
||||
51
apps/app-frontend/src/components/ui/SkinSiteSessionFrame.vue
Normal file
51
apps/app-frontend/src/components/ui/SkinSiteSessionFrame.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
receiveSkinSiteMessage,
|
||||
resetSkinSiteSession,
|
||||
setSkinSiteFrame,
|
||||
SKIN_SITE_ORIGIN,
|
||||
} from '@/composables/skin-site-session'
|
||||
|
||||
const frame = ref<HTMLIFrameElement>()
|
||||
let lastMessage = 0
|
||||
let expiryTimer: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
function connect() {
|
||||
const contentWindow = frame.value?.contentWindow ?? null
|
||||
setSkinSiteFrame(contentWindow)
|
||||
contentWindow?.postMessage({ type: 'starlight-skin-session-connect' }, SKIN_SITE_ORIGIN)
|
||||
}
|
||||
|
||||
function receive(event: MessageEvent) {
|
||||
if (receiveSkinSiteMessage(event, frame.value?.contentWindow ?? null)) lastMessage = Date.now()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('message', receive)
|
||||
connect()
|
||||
expiryTimer = setInterval(() => {
|
||||
if (lastMessage && Date.now() - lastMessage > 90_000) resetSkinSiteSession()
|
||||
}, 10_000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('message', receive)
|
||||
clearInterval(expiryTimer)
|
||||
setSkinSiteFrame(null)
|
||||
resetSkinSiteSession()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<iframe
|
||||
ref="frame"
|
||||
:src="`${SKIN_SITE_ORIGIN}/`"
|
||||
title="StarLight Skin Site session"
|
||||
aria-hidden="true"
|
||||
tabindex="-1"
|
||||
class="pointer-events-none fixed -left-[10000px] top-0 h-px w-px opacity-0"
|
||||
@load="connect"
|
||||
/>
|
||||
</template>
|
||||
@ -84,6 +84,7 @@ const props = defineProps<{
|
||||
isSkinActive: (skin: Skin) => boolean
|
||||
isAddSkinButtonDragActive: boolean
|
||||
readOnly?: boolean
|
||||
manageSavedSkins?: boolean
|
||||
activeTab?: 'saved' | 'default'
|
||||
}>()
|
||||
|
||||
@ -182,7 +183,9 @@ const sections = computed<SkinSection[]>(() => {
|
||||
|
||||
const draggableSavedSkins = ref<Skin[]>([])
|
||||
const isDraggingSavedSkin = ref(false)
|
||||
const canReorderSavedSkins = computed(() => draggableSavedSkins.value.length > 1)
|
||||
const canReorderSavedSkins = computed(
|
||||
() => props.manageSavedSkins !== false && draggableSavedSkins.value.length > 1,
|
||||
)
|
||||
const fixedSavedSkins = computed(() => props.savedSkins.filter((skin) => !canDragSavedSkin(skin)))
|
||||
|
||||
const sectionLayouts = computed(() => {
|
||||
@ -486,7 +489,7 @@ defineExpose({ getAddSkinButtonElement })
|
||||
:is-dragging="isDraggingSavedSkin"
|
||||
@select="emit('select', skin)"
|
||||
>
|
||||
<template v-if="!readOnly" #overlay-buttons>
|
||||
<template v-if="!readOnly && manageSavedSkins !== false" #overlay-buttons>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
@ -526,7 +529,7 @@ defineExpose({ getAddSkinButtonElement })
|
||||
:is-dragging="isDraggingSavedSkin"
|
||||
@select="emit('select', skin)"
|
||||
>
|
||||
<template v-if="!readOnly" #overlay-buttons>
|
||||
<template v-if="!readOnly && manageSavedSkins !== false" #overlay-buttons>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
@ -568,7 +571,7 @@ defineExpose({ getAddSkinButtonElement })
|
||||
:is-dragging="isDraggingSavedSkin"
|
||||
@select="emit('select', skin)"
|
||||
>
|
||||
<template #overlay-buttons>
|
||||
<template v-if="manageSavedSkins !== false" #overlay-buttons>
|
||||
<ButtonStyled color="brand">
|
||||
<button
|
||||
:aria-label="formatMessage(messages.editSkinButton)"
|
||||
|
||||
Reference in New Issue
Block a user