fix: use optimistic approach when changing skins/capes.
This commit is contained in:
committed by
Alejandro González
parent
34e49d40f4
commit
006611bd23
@@ -24,6 +24,7 @@
|
||||
"@tauri-apps/plugin-os": "^2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.7.1",
|
||||
"@tauri-apps/plugin-window-state": "^2.2.2",
|
||||
"@types/three": "^0.172.0",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"dayjs": "^1.11.10",
|
||||
|
||||
@@ -69,12 +69,7 @@ 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,
|
||||
get_normalized_skin_texture,
|
||||
normalize_skin_texture,
|
||||
} from './helpers/skins'
|
||||
import { get_available_capes, get_available_skins } from './helpers/skins'
|
||||
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
|
||||
|
||||
const formatRelativeTime = useRelativeTime()
|
||||
|
||||
@@ -77,13 +77,15 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 mt-12">
|
||||
<ButtonStyled color="brand" :disabled="disableSave">
|
||||
<Button v-tooltip="saveTooltip" :disabled="disableSave" @click="save">
|
||||
<CheckIcon v-if="mode === 'new'" /><SaveIcon v-else />
|
||||
<ButtonStyled color="brand" :disabled="disableSave || isSaving">
|
||||
<Button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
|
||||
<SpinnerIcon v-if="isSaving" class="animate-spin" />
|
||||
<CheckIcon v-else-if="mode === 'new'" />
|
||||
<SaveIcon v-else />
|
||||
{{ mode === 'new' ? 'Add skin' : 'Save skin' }}
|
||||
</Button>
|
||||
</ButtonStyled>
|
||||
<Button @click="hide"><XIcon />Cancel</Button>
|
||||
<Button :disabled="isSaving" @click="hide"><XIcon />Cancel</Button>
|
||||
</div>
|
||||
</NewModal>
|
||||
|
||||
@@ -117,14 +119,22 @@ import {
|
||||
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 {
|
||||
UploadIcon,
|
||||
CheckIcon,
|
||||
SaveIcon,
|
||||
XIcon,
|
||||
ChevronRightIcon,
|
||||
SpinnerIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { computedAsync } from '@vueuse/core'
|
||||
|
||||
const modal = useTemplateRef('modal')
|
||||
const selectCapeModal = useTemplateRef('selectCapeModal')
|
||||
const mode = ref<'new' | 'edit'>('new')
|
||||
const currentSkin = ref<Skin | null>(null)
|
||||
const shouldRestoreModal = ref(false)
|
||||
const isSaving = ref(false) // Add loading state
|
||||
|
||||
const uploadedTextureUrl = ref<string | null>(null)
|
||||
|
||||
@@ -192,6 +202,7 @@ const disableSave = computed(
|
||||
)
|
||||
|
||||
const saveTooltip = computed(() => {
|
||||
if (isSaving.value) return 'Saving...'
|
||||
if (mode.value === 'new' && !uploadedTextureUrl.value) return 'Upload a skin first!'
|
||||
if (mode.value === 'edit' && !hasEdits.value) return 'Make an edit to the skin first!'
|
||||
return undefined
|
||||
@@ -205,6 +216,7 @@ function resetState() {
|
||||
selectedCape.value = undefined
|
||||
visibleCapeList.value = []
|
||||
shouldRestoreModal.value = false
|
||||
isSaving.value = false
|
||||
}
|
||||
|
||||
function show(e: MouseEvent, skin?: Skin) {
|
||||
@@ -310,6 +322,8 @@ function openUploadSkinModal(e: MouseEvent) {
|
||||
}
|
||||
|
||||
async function save() {
|
||||
isSaving.value = true
|
||||
|
||||
try {
|
||||
let textureUrl: string
|
||||
|
||||
@@ -335,6 +349,8 @@ async function save() {
|
||||
hide()
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
} finally {
|
||||
isSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as THREE from 'three'
|
||||
import {Skin, Cape, normalize_skin_texture, get_normalized_skin_texture} from '../skins'
|
||||
import { determineModelType } from '../skins'
|
||||
import type { Skin, Cape } from '../skins'
|
||||
import { get_normalized_skin_texture, determineModelType } from '../skins'
|
||||
import { reactive } from 'vue'
|
||||
import { setupSkinModel, disposeCaches } from '@modrinth/utils'
|
||||
import { skinPreviewStorage } from '../storage/skin-preview-storage'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import {arrayBufferToBase64} from "@modrinth/utils";
|
||||
import { arrayBufferToBase64 } from '@modrinth/utils'
|
||||
|
||||
export interface Cape {
|
||||
id: string
|
||||
@@ -144,9 +144,9 @@ export async function remove_custom_skin(skin: Skin): Promise<void> {
|
||||
}
|
||||
|
||||
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}`;
|
||||
const data = await normalize_skin_texture(skin.texture)
|
||||
const base64 = arrayBufferToBase64(data)
|
||||
return `data:image/png;base64,${base64}`
|
||||
}
|
||||
|
||||
export async function normalize_skin_texture(texture: Uint8Array | string): Promise<Uint8Array> {
|
||||
|
||||
@@ -16,15 +16,16 @@ import {
|
||||
SkinLikeTextButton,
|
||||
SkinPreviewRenderer,
|
||||
} from '@modrinth/ui'
|
||||
import {computedAsync} from '@vueuse/core'
|
||||
import type {Ref} from 'vue'
|
||||
import {computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch} 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 {Cape, normalize_skin_texture, Skin} from '@/helpers/skins.ts'
|
||||
import { handleError } from '@/store/notifications'
|
||||
import type { Cape, Skin } from '@/helpers/skins.ts'
|
||||
import {
|
||||
normalize_skin_texture,
|
||||
equip_skin,
|
||||
filterDefaultSkins,
|
||||
filterSavedSkins,
|
||||
@@ -34,14 +35,14 @@ import {
|
||||
remove_custom_skin,
|
||||
set_default_cape,
|
||||
} from '@/helpers/skins.ts'
|
||||
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 { 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'
|
||||
import {arrayBufferToBase64} from "@modrinth/utils";
|
||||
import { arrayBufferToBase64 } from '@modrinth/utils'
|
||||
|
||||
const editSkinModal = useTemplateRef('editSkinModal')
|
||||
const selectCapeModal = useTemplateRef('selectCapeModal')
|
||||
@@ -59,6 +60,9 @@ const username = computed(() => currentUser.value?.profile?.name ?? undefined)
|
||||
const selectedSkin = ref<Skin | null>(null)
|
||||
const defaultCape = ref<Cape>()
|
||||
|
||||
const originalSelectedSkin = ref<Skin | null>(null)
|
||||
const originalDefaultCape = ref<Cape>()
|
||||
|
||||
const savedSkins = computed(() => filterSavedSkins(skins.value))
|
||||
const defaultSkins = computed(() => filterDefaultSkins(skins.value))
|
||||
|
||||
@@ -74,9 +78,9 @@ const currentCape = computed(() => {
|
||||
|
||||
const skinTexture = computedAsync(async () => {
|
||||
if (selectedSkin.value?.texture) {
|
||||
return await get_normalized_skin_texture(selectedSkin.value);
|
||||
return await get_normalized_skin_texture(selectedSkin.value)
|
||||
} else {
|
||||
return '';
|
||||
return ''
|
||||
}
|
||||
})
|
||||
const capeTexture = computed(() => currentCape.value?.texture)
|
||||
@@ -106,6 +110,7 @@ async function loadCapes() {
|
||||
try {
|
||||
capes.value = (await get_available_capes()) ?? []
|
||||
defaultCape.value = capes.value.find((c) => c.is_equipped)
|
||||
originalDefaultCape.value = defaultCape.value
|
||||
} catch (error) {
|
||||
if (currentUser.value) {
|
||||
handleError(error)
|
||||
@@ -118,6 +123,7 @@ async function loadSkins() {
|
||||
skins.value = (await get_available_skins()) ?? []
|
||||
generateSkinPreviews(skins.value, capes.value)
|
||||
selectedSkin.value = skins.value.find((s) => s.is_equipped) ?? null
|
||||
originalSelectedSkin.value = selectedSkin.value
|
||||
} catch (error) {
|
||||
if (currentUser.value) {
|
||||
handleError(error)
|
||||
@@ -126,14 +132,45 @@ async function loadSkins() {
|
||||
}
|
||||
|
||||
async function changeSkin(newSkin: Skin) {
|
||||
await equip_skin(newSkin).catch(handleError)
|
||||
await loadSkins()
|
||||
const previousSkin = selectedSkin.value
|
||||
const previousSkinsList = [...skins.value]
|
||||
|
||||
skins.value = skins.value.map((skin) => {
|
||||
return {
|
||||
...skin,
|
||||
is_equipped: skin.texture_key === newSkin.texture_key,
|
||||
}
|
||||
})
|
||||
|
||||
selectedSkin.value = skins.value.find((s) => s.texture_key === newSkin.texture_key) || null
|
||||
|
||||
try {
|
||||
await equip_skin(newSkin)
|
||||
} catch (error) {
|
||||
selectedSkin.value = previousSkin
|
||||
skins.value = previousSkinsList
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCapeSelected(cape: Cape | undefined) {
|
||||
await set_default_cape(cape).catch(handleError)
|
||||
await loadSkins()
|
||||
await loadCapes()
|
||||
const previousDefaultCape = defaultCape.value
|
||||
const previousCapesList = [...capes.value]
|
||||
|
||||
capes.value = capes.value.map((c) => ({
|
||||
...c,
|
||||
is_equipped: cape ? c.id === cape.id : false,
|
||||
}))
|
||||
|
||||
defaultCape.value = cape ? capes.value.find((c) => c.id === cape.id) : undefined
|
||||
|
||||
try {
|
||||
await set_default_cape(cape)
|
||||
} catch (error) {
|
||||
defaultCape.value = previousDefaultCape
|
||||
capes.value = previousCapesList
|
||||
handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function onSkinSaved() {
|
||||
@@ -178,8 +215,10 @@ function openUploadSkinModal(e: MouseEvent) {
|
||||
function onSkinFileUploaded(file: File) {
|
||||
const fakeEvent = new MouseEvent('click')
|
||||
file.arrayBuffer().then(async (buf) => {
|
||||
const skinTextureNormalized: Uint8Array = await normalize_skin_texture(`data:image/png;base64,` + arrayBufferToBase64(buf))
|
||||
const skinTexUrl = `data:image/png;base64,` + arrayBufferToBase64(skinTextureNormalized);
|
||||
const skinTextureNormalized: Uint8Array = await normalize_skin_texture(
|
||||
`data:image/png;base64,` + arrayBufferToBase64(buf),
|
||||
)
|
||||
const skinTexUrl = `data:image/png;base64,` + arrayBufferToBase64(skinTextureNormalized)
|
||||
|
||||
if (editSkinModal.value && editSkinModal.value.shouldRestoreModal) {
|
||||
editSkinModal.value.restoreWithNewTexture(skinTexUrl)
|
||||
|
||||
@@ -364,12 +364,6 @@ export function getPingLevel(ping: number) {
|
||||
}
|
||||
|
||||
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);
|
||||
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)
|
||||
return btoa(String.fromCharCode(...bytes))
|
||||
}
|
||||
|
||||
129
pnpm-lock.yaml
generated
129
pnpm-lock.yaml
generated
@@ -77,15 +77,21 @@ importers:
|
||||
'@tauri-apps/plugin-window-state':
|
||||
specifier: ^2.2.2
|
||||
version: 2.2.2
|
||||
'@types/three':
|
||||
specifier: ^0.172.0
|
||||
version: 0.172.0
|
||||
'@vintl/vintl':
|
||||
specifier: ^4.4.1
|
||||
version: 4.4.1(typescript@5.5.4)(vue@3.5.13(typescript@5.5.4))
|
||||
'@vueuse/core':
|
||||
specifier: ^11.1.0
|
||||
version: 11.1.0(vue@3.5.13(typescript@5.5.4))
|
||||
dayjs:
|
||||
specifier: ^1.11.10
|
||||
version: 1.11.11
|
||||
floating-vue:
|
||||
specifier: ^5.2.2
|
||||
version: 5.2.2(@nuxt/kit@3.14.1592(magicast@0.3.5))(vue@3.5.13(typescript@5.5.4))
|
||||
version: 5.2.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(vue@3.5.13(typescript@5.5.4))
|
||||
ofetch:
|
||||
specifier: ^1.3.4
|
||||
version: 1.4.1
|
||||
@@ -9489,34 +9495,6 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@nuxt/kit@3.14.1592(magicast@0.3.5)':
|
||||
dependencies:
|
||||
'@nuxt/schema': 3.14.1592(magicast@0.3.5)
|
||||
c12: 2.0.1(magicast@0.3.5)
|
||||
consola: 3.2.3
|
||||
defu: 6.1.4
|
||||
destr: 2.0.3
|
||||
globby: 14.0.2
|
||||
hash-sum: 2.0.0
|
||||
ignore: 6.0.2
|
||||
jiti: 2.4.1
|
||||
klona: 2.0.6
|
||||
knitwork: 1.1.0
|
||||
mlly: 1.7.3
|
||||
pathe: 1.1.2
|
||||
pkg-types: 1.2.1
|
||||
scule: 1.3.0
|
||||
semver: 7.7.1
|
||||
ufo: 1.5.4
|
||||
unctx: 2.3.1
|
||||
unimport: 3.14.4
|
||||
untyped: 1.5.1
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
- rollup
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
'@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@3.29.4)':
|
||||
dependencies:
|
||||
'@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@3.29.4)
|
||||
@@ -9572,20 +9550,27 @@ snapshots:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
'@nuxt/schema@3.14.1592(magicast@0.3.5)':
|
||||
'@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9)':
|
||||
dependencies:
|
||||
'@nuxt/schema': 3.14.1592(magicast@0.3.5)(rollup@4.34.9)
|
||||
c12: 2.0.1(magicast@0.3.5)
|
||||
compatx: 0.1.8
|
||||
consola: 3.2.3
|
||||
defu: 6.1.4
|
||||
hookable: 5.5.3
|
||||
destr: 2.0.3
|
||||
globby: 14.0.2
|
||||
hash-sum: 2.0.0
|
||||
ignore: 6.0.2
|
||||
jiti: 2.4.1
|
||||
klona: 2.0.6
|
||||
knitwork: 1.1.0
|
||||
mlly: 1.7.3
|
||||
pathe: 1.1.2
|
||||
pkg-types: 1.2.1
|
||||
scule: 1.3.0
|
||||
std-env: 3.8.0
|
||||
semver: 7.7.1
|
||||
ufo: 1.5.4
|
||||
uncrypto: 0.1.3
|
||||
unimport: 3.14.4
|
||||
unctx: 2.3.1
|
||||
unimport: 3.14.4(rollup@4.34.9)
|
||||
untyped: 1.5.1
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
@@ -9634,6 +9619,27 @@ snapshots:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
'@nuxt/schema@3.14.1592(magicast@0.3.5)(rollup@4.34.9)':
|
||||
dependencies:
|
||||
c12: 2.0.1(magicast@0.3.5)
|
||||
compatx: 0.1.8
|
||||
consola: 3.2.3
|
||||
defu: 6.1.4
|
||||
hookable: 5.5.3
|
||||
pathe: 1.1.2
|
||||
pkg-types: 1.2.1
|
||||
scule: 1.3.0
|
||||
std-env: 3.8.0
|
||||
ufo: 1.5.4
|
||||
uncrypto: 0.1.3
|
||||
unimport: 3.14.4(rollup@4.34.9)
|
||||
untyped: 1.5.1
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
- rollup
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
'@nuxt/telemetry@2.6.0(magicast@0.3.5)(rollup@4.28.1)':
|
||||
dependencies:
|
||||
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.28.1)
|
||||
@@ -9976,6 +9982,15 @@ snapshots:
|
||||
optionalDependencies:
|
||||
rollup: 4.28.1
|
||||
|
||||
'@rollup/pluginutils@5.1.3(rollup@4.34.9)':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.6
|
||||
estree-walker: 2.0.2
|
||||
picomatch: 4.0.2
|
||||
optionalDependencies:
|
||||
rollup: 4.34.9
|
||||
optional: true
|
||||
|
||||
'@rollup/pluginutils@5.1.4(rollup@4.34.9)':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.6
|
||||
@@ -13081,13 +13096,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.28.1)
|
||||
|
||||
floating-vue@5.2.2(@nuxt/kit@3.14.1592(magicast@0.3.5))(vue@3.5.13(typescript@5.5.4)):
|
||||
floating-vue@5.2.2(@nuxt/kit@3.14.1592(magicast@0.3.5)(rollup@4.34.9))(vue@3.5.13(typescript@5.5.4)):
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.1.1
|
||||
vue: 3.5.13(typescript@5.5.4)
|
||||
vue-resize: 2.0.0-alpha.1(vue@3.5.13(typescript@5.5.4))
|
||||
optionalDependencies:
|
||||
'@nuxt/kit': 3.14.1592(magicast@0.3.5)
|
||||
'@nuxt/kit': 3.14.1592(magicast@0.3.5)(rollup@4.34.9)
|
||||
|
||||
for-each@0.3.3:
|
||||
dependencies:
|
||||
@@ -16704,26 +16719,6 @@ snapshots:
|
||||
trough: 2.2.0
|
||||
vfile: 6.0.3
|
||||
|
||||
unimport@3.14.4:
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.3(rollup@4.28.1)
|
||||
acorn: 8.14.0
|
||||
escape-string-regexp: 5.0.0
|
||||
estree-walker: 3.0.3
|
||||
local-pkg: 0.5.1
|
||||
magic-string: 0.30.14
|
||||
mlly: 1.7.3
|
||||
pathe: 1.1.2
|
||||
picomatch: 4.0.2
|
||||
pkg-types: 1.2.1
|
||||
scule: 1.3.0
|
||||
strip-literal: 2.1.1
|
||||
tinyglobby: 0.2.10
|
||||
unplugin: 1.16.0
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
optional: true
|
||||
|
||||
unimport@3.14.4(rollup@3.29.4):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.3(rollup@3.29.4)
|
||||
@@ -16763,6 +16758,26 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
|
||||
unimport@3.14.4(rollup@4.34.9):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.1.3(rollup@4.34.9)
|
||||
acorn: 8.14.0
|
||||
escape-string-regexp: 5.0.0
|
||||
estree-walker: 3.0.3
|
||||
local-pkg: 0.5.1
|
||||
magic-string: 0.30.14
|
||||
mlly: 1.7.3
|
||||
pathe: 1.1.2
|
||||
picomatch: 4.0.2
|
||||
pkg-types: 1.2.1
|
||||
scule: 1.3.0
|
||||
strip-literal: 2.1.1
|
||||
tinyglobby: 0.2.10
|
||||
unplugin: 1.16.0
|
||||
transitivePeerDependencies:
|
||||
- rollup
|
||||
optional: true
|
||||
|
||||
unist-util-find-after@5.0.0:
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
Reference in New Issue
Block a user