From 6c5480e0023d54a43e5e567c68839f54afa163e4 Mon Sep 17 00:00:00 2001 From: "Calum H." Date: Thu, 29 May 2025 17:48:29 +0100 Subject: [PATCH] fix: normalization --- apps/app-frontend/src/App.vue | 14 ++++- .../src/components/ui/skin/EditSkinModal.vue | 16 +++-- .../components/ui/skin/UploadSkinModal.vue | 8 ++- .../helpers/rendering/batch-skin-renderer.ts | 6 +- apps/app-frontend/src/helpers/skins.ts | 12 ++-- apps/app-frontend/src/pages/Skins.vue | 58 +++++++++++-------- packages/utils/utils.ts | 11 ++++ 7 files changed, 81 insertions(+), 44 deletions(-) diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 8a86df971..24b53d682 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -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) { diff --git a/apps/app-frontend/src/components/ui/skin/EditSkinModal.vue b/apps/app-frontend/src/components/ui/skin/EditSkinModal.vue index 1762840ef..5cc42ac26 100644 --- a/apps/app-frontend/src/components/ui/skin/EditSkinModal.vue +++ b/apps/app-frontend/src/components/ui/skin/EditSkinModal.vue @@ -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(() => { diff --git a/apps/app-frontend/src/components/ui/skin/UploadSkinModal.vue b/apps/app-frontend/src/components/ui/skin/UploadSkinModal.vue index 713aaf7c2..6382c514d 100644 --- a/apps/app-frontend/src/components/ui/skin/UploadSkinModal.vue +++ b/apps/app-frontend/src/components/ui/skin/UploadSkinModal.vue @@ -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 { 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 { } 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 diff --git a/apps/app-frontend/src/helpers/rendering/batch-skin-renderer.ts b/apps/app-frontend/src/helpers/rendering/batch-skin-renderer.ts index d40418183..714546cc3 100644 --- a/apps/app-frontend/src/helpers/rendering/batch-skin-renderer.ts +++ b/apps/app-frontend/src/helpers/rendering/batch-skin-renderer.ts @@ -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 { } } -export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise { +export async function generateSkinPreviews(skins: (Skin & { normalized_skin_texture: string })[], capes: Cape[]): Promise { 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, diff --git a/apps/app-frontend/src/helpers/skins.ts b/apps/app-frontend/src/helpers/skins.ts index 6c4298fa1..91c923441 100644 --- a/apps/app-frontend/src/helpers/skins.ts +++ b/apps/app-frontend/src/helpers/skins.ts @@ -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 { }) } -export async function get_normalized_skin_texture_url(skin: Skin): Promise { - 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 { + 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 { - return await invoke('plugin:minecraft-skins|normalize_skin_texture', { skin }) +export async function normalize_skin_texture(texture: Uint8Array | string): Promise { + return await invoke('plugin:minecraft-skins|normalize_skin_texture', { texture }) } export async function unequip_skin(): Promise { diff --git a/apps/app-frontend/src/pages/Skins.vue b/apps/app-frontend/src/pages/Skins.vue index 0bd27d8b3..bf0992986 100644 --- a/apps/app-frontend/src/pages/Skins.vue +++ b/apps/app-frontend/src/pages/Skins.vue @@ -1,43 +1,45 @@