diff --git a/apps/app-frontend/src/components/ui/skin/VirtualSkinSectionList.vue b/apps/app-frontend/src/components/ui/skin/VirtualSkinSectionList.vue index 999672604..2d0eda22e 100644 --- a/apps/app-frontend/src/components/ui/skin/VirtualSkinSectionList.vue +++ b/apps/app-frontend/src/components/ui/skin/VirtualSkinSectionList.vue @@ -159,9 +159,7 @@ const sections = computed(() => [ const draggableSavedSkins = ref([]) const isDraggingSavedSkin = ref(false) const canReorderSavedSkins = computed(() => draggableSavedSkins.value.length > 1) -const fixedSavedSkins = computed(() => - props.savedSkins.filter((skin) => !canPersistSkinOrder(skin)), -) +const fixedSavedSkins = computed(() => props.savedSkins.filter((skin) => !canDragSavedSkin(skin))) const sectionLayouts = computed(() => { const layouts: Array<{ section: SkinSection; top: number; height: number; index: number }> = [] @@ -226,7 +224,7 @@ watch( return } - draggableSavedSkins.value = nextSkins.filter(canPersistSkinOrder) + draggableSavedSkins.value = nextSkins.filter(canDragSavedSkin) }, { immediate: true }, ) @@ -283,17 +281,17 @@ function savedSkinKey(skin: Skin) { return skinKey(skin, 'saved-skin') } -function canPersistSkinOrder(skin: Skin) { - return skin.source === 'custom' +function canDragSavedSkin(skin: Skin) { + return skin.source === 'custom' || skin.source === 'custom_external' } function doSkinOrdersMatch(firstSkins: Skin[], secondSkins: Skin[]) { - const persistedSecondSkins = secondSkins.filter(canPersistSkinOrder) + const draggableSecondSkins = secondSkins.filter(canDragSavedSkin) return ( - firstSkins.length === persistedSecondSkins.length && + firstSkins.length === draggableSecondSkins.length && firstSkins.every( - (skin, index) => savedSkinKey(skin) === savedSkinKey(persistedSecondSkins[index]), + (skin, index) => savedSkinKey(skin) === savedSkinKey(draggableSecondSkins[index]), ) ) } @@ -306,7 +304,7 @@ function onSavedSkinDragEnd() { isDraggingSavedSkin.value = false if (doSkinOrdersMatch(draggableSavedSkins.value, props.savedSkins)) { - draggableSavedSkins.value = props.savedSkins.filter(canPersistSkinOrder) + draggableSavedSkins.value = props.savedSkins.filter(canDragSavedSkin) return } diff --git a/apps/app-frontend/src/pages/Skins.vue b/apps/app-frontend/src/pages/Skins.vue index fcb9ad686..71a593a24 100644 --- a/apps/app-frontend/src/pages/Skins.vue +++ b/apps/app-frontend/src/pages/Skins.vue @@ -46,6 +46,7 @@ import { get_normalized_skin_texture, normalize_skin_texture, remove_custom_skin, + save_custom_skin, set_custom_skin_order, } from '@/helpers/skins.ts' import { hasPride26Badge } from '@/helpers/user-campaigns.ts' @@ -399,6 +400,14 @@ function skinsMatch(a?: Skin | null, b?: Skin | null) { ) } +function skinsMatchIgnoringSource(a?: Skin | null, b?: Skin | null) { + return ( + a?.texture_key === b?.texture_key && + a?.variant === b?.variant && + (a?.cape_id ?? null) === (b?.cape_id ?? null) + ) +} + function isSkinSelected(skin: Skin) { return skinsMatch(selectedSkin.value, skin) } @@ -572,6 +581,8 @@ function updateLocalSkin(savedSkin: Skin, applied: boolean, previousSkin?: Skin) async function reorderSavedSkins(orderedSkins: Skin[]) { const previousSkins = skins.value + const previousSelectedSkin = selectedSkin.value + const previousOriginalSelectedSkin = originalSelectedSkin.value const orderedTextureKeys = orderedSkins.map((skin) => skin.texture_key) const orderedTextureKeySet = new Set(orderedTextureKeys) const remainingSavedSkins = previousSkins.filter( @@ -584,11 +595,22 @@ async function reorderSavedSkins(orderedSkins: Skin[]) { generateSkinPreviews(skins.value, capes.value) try { + const persistedSavedSkins = await preserveExternalSkins(nextSavedSkins) + + if (persistedSavedSkins.some((skin, index) => skin !== nextSavedSkins[index])) { + skins.value = [...persistedSavedSkins, ...defaultSkins] + generateSkinPreviews(skins.value, capes.value) + } + await set_custom_skin_order( - nextSavedSkins.filter((skin) => skin.source === 'custom').map((skin) => skin.texture_key), + persistedSavedSkins + .filter((skin) => skin.source === 'custom') + .map((skin) => skin.texture_key), ) } catch (error) { skins.value = previousSkins + selectedSkin.value = previousSelectedSkin + originalSelectedSkin.value = previousOriginalSelectedSkin generateSkinPreviews(skins.value, capes.value) addNotification({ type: 'error', @@ -599,6 +621,39 @@ async function reorderSavedSkins(orderedSkins: Skin[]) { } } +async function preserveExternalSkins(skinsToPersist: Skin[]) { + const preservedSkins: Skin[] = [] + + for (const skin of skinsToPersist) { + if (skin.source !== 'custom_external') { + preservedSkins.push(skin) + continue + } + + const textureBlob = await normalize_skin_texture(skin.texture) + const capeId = skin.cape_id ? capes.value.find((cape) => cape.id === skin.cape_id) : undefined + const savedSkin = await save_custom_skin(skin, textureBlob, skin.variant, capeId, false) + const preservedSkin: Skin = { + ...savedSkin, + source: 'custom', + is_equipped: skin.is_equipped, + } + + if (skinsMatchIgnoringSource(selectedSkin.value, skin)) { + selectedSkin.value = preservedSkin + } + + if (skinsMatchIgnoringSource(originalSelectedSkin.value, skin)) { + originalSelectedSkin.value = preservedSkin + void accountsCard.value?.setEquippedSkin(preservedSkin) + } + + preservedSkins.push(preservedSkin) + } + + return preservedSkins +} + function schedulePendingSkinRefresh() { if (pendingSkinRefreshTimeout !== null) { window.clearTimeout(pendingSkinRefreshTimeout)