fix: normalization
This commit is contained in:
committed by
Alejandro González
parent
92f281a6df
commit
6c5480e002
@@ -69,7 +69,12 @@ import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
|
||||
import FriendsList from '@/components/ui/friends/FriendsList.vue'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
||||
import { get_available_capes, get_available_skins } from './helpers/skins'
|
||||
import {
|
||||
get_available_capes,
|
||||
get_available_skins,
|
||||
get_normalized_skin_texture,
|
||||
normalize_skin_texture,
|
||||
} from './helpers/skins'
|
||||
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
||||
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
@@ -202,7 +207,12 @@ async function setupApp() {
|
||||
fetchCredentials()
|
||||
|
||||
try {
|
||||
const skins = (await get_available_skins()) ?? []
|
||||
const skins = (await get_available_skins()).map(async skin => {
|
||||
return {
|
||||
...skin,
|
||||
normalized_texture: await get_normalized_skin_texture(skin.texture)
|
||||
}
|
||||
}) ?? []
|
||||
const capes = (await get_available_capes()) ?? []
|
||||
generateSkinPreviews(skins, capes)
|
||||
} catch (error) {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
wide-model-src="/src/assets/models/classic_player.gltf"
|
||||
cape-model-src="/src/assets/models/cape.gltf"
|
||||
:variant="variant"
|
||||
:texture-src="previewSkin"
|
||||
:texture-src="previewSkin || ''"
|
||||
:cape-src="selectedCapeTexture"
|
||||
:scale="1.4"
|
||||
:fov="50"
|
||||
@@ -113,10 +113,12 @@ import {
|
||||
unequip_skin,
|
||||
type Skin,
|
||||
type Cape,
|
||||
type SkinModel,
|
||||
type SkinModel, normalize_skin_texture, get_normalized_skin_texture,
|
||||
} from '@/helpers/skins.ts'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import { UploadIcon, CheckIcon, SaveIcon, XIcon, ChevronRightIcon } from '@modrinth/assets'
|
||||
import {computedAsync} from "@vueuse/core";
|
||||
import {arrayBufferToBase64} from "@modrinth/utils";
|
||||
|
||||
const modal = useTemplateRef('modal')
|
||||
const selectCapeModal = useTemplateRef('selectCapeModal')
|
||||
@@ -179,10 +181,12 @@ watch(fileUploadTextureBlob, (blob, prev) => {
|
||||
if (blob) localPreviewUrl.value = URL.createObjectURL(new Blob([blob]))
|
||||
else localPreviewUrl.value = null
|
||||
})
|
||||
const previewSkin = computed(() => {
|
||||
if (localPreviewUrl.value) return localPreviewUrl.value
|
||||
if (currentSkin.value) return currentSkin.value.texture
|
||||
return '/src/assets/skins/steve.png'
|
||||
const previewSkin = computedAsync(async () => {
|
||||
if (localPreviewUrl.value)
|
||||
return localPreviewUrl.value;
|
||||
else if (currentSkin.value) {
|
||||
return await get_normalized_skin_texture(currentSkin.value);
|
||||
} else return '/src/assets/skins/steve.png'
|
||||
})
|
||||
|
||||
const hasEdits = computed(() => {
|
||||
|
||||
@@ -47,6 +47,7 @@ const emit = defineEmits<{
|
||||
function show(e?: MouseEvent) {
|
||||
modal.value?.show(e)
|
||||
}
|
||||
|
||||
function hide(emitCanceled = false) {
|
||||
modal.value?.hide()
|
||||
resetState()
|
||||
@@ -54,9 +55,11 @@ function hide(emitCanceled = false) {
|
||||
emit('canceled')
|
||||
}
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
if (fileInput.value) fileInput.value.value = ''
|
||||
}
|
||||
|
||||
function triggerFileInput() {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
@@ -66,7 +69,7 @@ async function validateImageDimensions(file: File): Promise<boolean> {
|
||||
const img = new Image()
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(img.src)
|
||||
resolve(img.width === 64 && img.height === 64)
|
||||
resolve(img.width === 64 && (img.height === 64 || img.height == 32))
|
||||
}
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(img.src)
|
||||
@@ -77,7 +80,6 @@ async function validateImageDimensions(file: File): Promise<boolean> {
|
||||
}
|
||||
|
||||
async function handleFileOperation(e: Event | DragEvent) {
|
||||
// Get files from either drag event or file input
|
||||
const files = (e as DragEvent).dataTransfer?.files || (e.target as HTMLInputElement).files
|
||||
if (!files || files.length === 0) {
|
||||
return
|
||||
@@ -95,7 +97,7 @@ async function handleFileOperation(e: Event | DragEvent) {
|
||||
if (!isValidDimensions) {
|
||||
notifications.addNotification({
|
||||
title: 'Invalid dimensions.',
|
||||
text: 'Only 64x64 PNG files are accepted.',
|
||||
text: 'Only 64x64 and 64x32 PNG files are accepted.',
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as THREE from 'three'
|
||||
import {Skin, Cape, normalize_skin_texture, get_normalized_skin_texture_url} from '../skins'
|
||||
import {Skin, Cape, normalize_skin_texture, get_normalized_skin_texture} from '../skins'
|
||||
import { determineModelType } from '../skins'
|
||||
import { reactive } from 'vue'
|
||||
import { setupSkinModel, disposeCaches } from '@modrinth/utils'
|
||||
@@ -145,7 +145,7 @@ export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
|
||||
export async function generateSkinPreviews(skins: (Skin & { normalized_skin_texture: string })[], capes: Cape[]): Promise<void> {
|
||||
const renderer = new BatchSkinRenderer()
|
||||
const capeModelUrl = '/src/assets/models/cape.gltf'
|
||||
|
||||
@@ -185,7 +185,7 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
|
||||
const modelUrl = getModelUrlForVariant(variant)
|
||||
const cape: Cape | undefined = capes.find((_cape) => _cape.id === skin.cape_id)
|
||||
const renderResult = await renderer.renderSkin(
|
||||
await get_normalized_skin_texture_url(skin),
|
||||
skin.normalized_skin_texture,
|
||||
modelUrl,
|
||||
cape?.texture,
|
||||
capeModelUrl,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import {arrayBufferToBase64} from "@modrinth/utils";
|
||||
|
||||
export interface Cape {
|
||||
id: string
|
||||
@@ -142,13 +143,14 @@ export async function remove_custom_skin(skin: Skin): Promise<void> {
|
||||
})
|
||||
}
|
||||
|
||||
export async function get_normalized_skin_texture_url(skin: Skin): Promise<string> {
|
||||
const bytes: Uint8Array = await normalize_skin_texture(skin);
|
||||
return `data:image/png;base64,` + Buffer.from(bytes).toString("base64");
|
||||
export async function get_normalized_skin_texture(skin: Skin): Promise<string> {
|
||||
const data = await normalize_skin_texture(skin.texture);
|
||||
const base64 = arrayBufferToBase64(data);
|
||||
return `data:image/png;base64,${base64}`;
|
||||
}
|
||||
|
||||
export async function normalize_skin_texture(skin: Skin): Promise<Uint8Array> {
|
||||
return await invoke('plugin:minecraft-skins|normalize_skin_texture', { skin })
|
||||
export async function normalize_skin_texture(texture: Uint8Array | string): Promise<Uint8Array> {
|
||||
return await invoke('plugin:minecraft-skins|normalize_skin_texture', { texture })
|
||||
}
|
||||
|
||||
export async function unequip_skin(): Promise<void> {
|
||||
|
||||
@@ -1,43 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
UpdatedIcon,
|
||||
PlusIcon,
|
||||
EditIcon,
|
||||
ExcitedRinthbot,
|
||||
LogInIcon,
|
||||
PlusIcon,
|
||||
SpinnerIcon,
|
||||
EditIcon,
|
||||
TrashIcon,
|
||||
UpdatedIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Button,
|
||||
ButtonStyled,
|
||||
SkinPreviewRenderer,
|
||||
ConfirmModal,
|
||||
SkinButton,
|
||||
SkinLikeTextButton,
|
||||
Button,
|
||||
ConfirmModal,
|
||||
SkinPreviewRenderer,
|
||||
} from '@modrinth/ui'
|
||||
import type { Ref } from 'vue'
|
||||
import { ref, computed, useTemplateRef, watch, onMounted, onUnmounted, inject } from 'vue'
|
||||
import {computedAsync} from '@vueuse/core'
|
||||
import type {Ref} from 'vue'
|
||||
import {computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch} from 'vue'
|
||||
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
|
||||
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
|
||||
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import {handleError} from '@/store/notifications'
|
||||
import {Cape, normalize_skin_texture, Skin} from '@/helpers/skins.ts'
|
||||
import {
|
||||
get_available_skins,
|
||||
get_available_capes,
|
||||
filterSavedSkins,
|
||||
filterDefaultSkins,
|
||||
equip_skin,
|
||||
set_default_cape,
|
||||
filterDefaultSkins,
|
||||
filterSavedSkins,
|
||||
get_available_capes,
|
||||
get_available_skins,
|
||||
get_normalized_skin_texture,
|
||||
remove_custom_skin,
|
||||
set_default_cape,
|
||||
} from '@/helpers/skins.ts'
|
||||
import { get as getSettings } from '@/helpers/settings.ts'
|
||||
import type { Cape, Skin } from '@/helpers/skins.ts'
|
||||
import { get_default_user, users, login as login_flow } from '@/helpers/auth'
|
||||
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import { generateSkinPreviews, map } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import { handleSevereError } from '@/store/error'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import {get as getSettings} from '@/helpers/settings.ts'
|
||||
import {get_default_user, login as login_flow, users} from '@/helpers/auth'
|
||||
import type {RenderResult} from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import {generateSkinPreviews, map} from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||
import {handleSevereError} from '@/store/error'
|
||||
import {trackEvent} from '@/helpers/analytics'
|
||||
import type AccountsCard from '@/components/ui/AccountsCard.vue'
|
||||
|
||||
const editSkinModal = useTemplateRef('editSkinModal')
|
||||
@@ -69,7 +71,13 @@ const currentCape = computed(() => {
|
||||
return defaultCape.value
|
||||
})
|
||||
|
||||
const skinTexture = computed(() => selectedSkin.value?.texture ?? '')
|
||||
const skinTexture = computedAsync(async () => {
|
||||
if (selectedSkin.value?.texture) {
|
||||
return await get_normalized_skin_texture(selectedSkin.value);
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
})
|
||||
const capeTexture = computed(() => currentCape.value?.texture)
|
||||
const skinVariant = computed(() => selectedSkin.value?.variant)
|
||||
const skinNametag = computed(() =>
|
||||
@@ -168,8 +176,8 @@ function openUploadSkinModal(e: MouseEvent) {
|
||||
|
||||
function onSkinFileUploaded(file: File) {
|
||||
const fakeEvent = new MouseEvent('click')
|
||||
file.arrayBuffer().then((buf) => {
|
||||
const skinTexture = new Uint8Array(buf)
|
||||
file.arrayBuffer().then(async (buf) => {
|
||||
const skinTexture: Uint8Array = await normalize_skin_texture(new Uint8Array(buf))
|
||||
|
||||
if (editSkinModal.value && editSkinModal.value.shouldRestoreModal) {
|
||||
editSkinModal.value.restoreWithNewTexture(skinTexture, file.name)
|
||||
@@ -253,7 +261,7 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
|
||||
slim-model-src="/src/assets/models/slim_player.gltf"
|
||||
cape-model-src="/src/assets/models/cape.gltf"
|
||||
:cape-src="capeTexture"
|
||||
:texture-src="skinTexture"
|
||||
:texture-src="skinTexture || ''"
|
||||
:variant="skinVariant"
|
||||
:nametag="skinNametag"
|
||||
:initial-rotation="Math.PI / 8"
|
||||
|
||||
@@ -362,3 +362,14 @@ export function getPingLevel(ping: number) {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
export function arrayBufferToBase64(buffer: Uint8Array | ArrayBuffer): string {
|
||||
const bytes = buffer instanceof Uint8Array
|
||||
? buffer
|
||||
: new Uint8Array(buffer);
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user