feat: Finish new skin modal
This commit is contained in:
committed by
Alejandro González
parent
fd6e263e0a
commit
d8cb19bf40
BIN
apps/app-frontend/src/assets/skins/herobrine.png
Normal file
BIN
apps/app-frontend/src/assets/skins/herobrine.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
apps/app-frontend/src/assets/skins/steve.png
Normal file
BIN
apps/app-frontend/src/assets/skins/steve.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
@@ -7,19 +7,19 @@ import {
|
||||
SkinPreviewRenderer,
|
||||
CapeButton,
|
||||
CapeLikeTextButton,
|
||||
Button, RadioButtons
|
||||
Button, RadioButtons, ButtonStyled
|
||||
} from '@modrinth/ui'
|
||||
import {
|
||||
add_and_equip_custom_skin,
|
||||
type Cape,
|
||||
determineModelType,
|
||||
equip_skin,
|
||||
equip_skin, remove_custom_skin,
|
||||
type Skin,
|
||||
type SkinModel
|
||||
} from '@/helpers/skins.ts'
|
||||
import {handleError} from '@/store/notifications'
|
||||
import {computed, ref, useTemplateRef, watch} from 'vue'
|
||||
import {CheckCircleIcon, InfoIcon, UploadIcon, ChevronRightIcon} from "@modrinth/assets";
|
||||
import {CheckCircleIcon, InfoIcon, UploadIcon, ChevronRightIcon, CheckIcon, SaveIcon, XIcon, TrashIcon } from "@modrinth/assets";
|
||||
|
||||
const modal = useTemplateRef('modal')
|
||||
const mode = ref<'new' | 'edit'>('new')
|
||||
@@ -27,6 +27,7 @@ const currentSkin = ref<Skin | null>(null)
|
||||
const textureBlob = ref<Uint8Array | null>(null)
|
||||
const variant = ref<SkinModel>('Classic')
|
||||
const selectedCape = ref<Cape | undefined>(undefined)
|
||||
const tempSkinUrl = ref<string>('/src/assets/skins/steve.png')
|
||||
|
||||
const props = defineProps<{ capes?: Cape[] }>()
|
||||
const firstThreeCapes = computed(() => props.capes?.slice(0, 3) ?? [])
|
||||
@@ -49,27 +50,30 @@ watch(textureBlob, async (blob, prev) => {
|
||||
|
||||
const previewSkin = computed(() => {
|
||||
if (localPreviewUrl.value) return localPreviewUrl.value
|
||||
if (currentSkin.value) return currentSkin.value.texture;
|
||||
if (currentSkin.value) return currentSkin.value.texture
|
||||
if (mode.value === 'new') return tempSkinUrl.value
|
||||
return ''
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'view-more-capes', ev: MouseEvent): void
|
||||
(e: 'saved'): void
|
||||
(e: 'saved', editedSkin: Skin | null, oldSkin: Skin): void
|
||||
(e: 'deleted', deletedSkin: Skin): void
|
||||
}>()
|
||||
|
||||
const disableSave = computed(() => mode.value === 'new' && !textureBlob.value)
|
||||
|
||||
function resetState() {
|
||||
mode.value = 'new'
|
||||
currentSkin.value = null
|
||||
textureBlob.value = null
|
||||
fileName.value = null
|
||||
mode.value = 'new'
|
||||
currentSkin.value = null
|
||||
textureBlob.value = null
|
||||
fileName.value = null
|
||||
|
||||
if (localPreviewUrl.value) {
|
||||
URL.revokeObjectURL(localPreviewUrl.value)
|
||||
localPreviewUrl.value = null
|
||||
}
|
||||
|
||||
variant.value = 'Classic'
|
||||
variant.value = 'Classic'
|
||||
selectedCape.value = undefined
|
||||
}
|
||||
|
||||
@@ -77,6 +81,13 @@ function show(e: MouseEvent, skin?: Skin) {
|
||||
mode.value = skin ? 'edit' : 'new'
|
||||
currentSkin.value = skin ?? null
|
||||
textureBlob.value = null
|
||||
|
||||
if (!skin) {
|
||||
tempSkinUrl.value = Math.random() <= 0.05
|
||||
? '/src/assets/skins/herobrine.png'
|
||||
: '/src/assets/skins/steve.png'
|
||||
}
|
||||
|
||||
variant.value = skin?.variant ?? 'Classic'
|
||||
selectedCape.value = skin?.cape_id ? props.capes?.find(c => c.id === skin.cape_id) : undefined
|
||||
modal.value?.show(e)
|
||||
@@ -84,7 +95,7 @@ function show(e: MouseEvent, skin?: Skin) {
|
||||
|
||||
function hide() {
|
||||
modal.value?.hide()
|
||||
resetState()
|
||||
setTimeout(resetState, 100);
|
||||
}
|
||||
|
||||
async function onTextureSelected(files: FileList | null) {
|
||||
@@ -104,29 +115,40 @@ function selectCape(cape: Cape | undefined) {
|
||||
}
|
||||
|
||||
function viewMoreCapes(e: MouseEvent) {
|
||||
hide()
|
||||
emit('view-more-capes', e)
|
||||
// TODO: later
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
await remove_custom_skin(currentSkin.value!)
|
||||
emit('deleted', currentSkin.value!)
|
||||
hide();
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
let edited: Skin | null = null;
|
||||
if (mode.value === 'new') {
|
||||
if (!textureBlob.value) throw new Error('Please upload a skin texture first.')
|
||||
await add_and_equip_custom_skin(textureBlob.value, variant.value, selectedCape.value).catch(handleError)
|
||||
await add_and_equip_custom_skin(textureBlob.value, variant.value, selectedCape.value)
|
||||
} else if (currentSkin.value) {
|
||||
const edited: Skin = {
|
||||
edited = {
|
||||
...currentSkin.value,
|
||||
variant: variant.value,
|
||||
cape_id: selectedCape.value?.id
|
||||
cape_id: selectedCape.value?.id,
|
||||
is_equipped: true
|
||||
}
|
||||
if (textureBlob.value) {
|
||||
await add_and_equip_custom_skin(textureBlob.value, variant.value, selectedCape.value).catch(handleError)
|
||||
await add_and_equip_custom_skin(textureBlob.value, variant.value, selectedCape.value)
|
||||
} else {
|
||||
await equip_skin(edited).catch(handleError)
|
||||
await equip_skin(edited);
|
||||
}
|
||||
}
|
||||
emit('saved', edited, currentSkin.value!)
|
||||
hide()
|
||||
emit('saved')
|
||||
} catch (err) {
|
||||
handleError(err)
|
||||
}
|
||||
@@ -159,8 +181,8 @@ defineExpose({
|
||||
wide-model-src="/src/assets/models/classic_player.gltf"
|
||||
:variant="variant"
|
||||
:texture-src="previewSkin"
|
||||
scale="1.4"
|
||||
fov="50"
|
||||
:scale="1.4"
|
||||
:fov="50"
|
||||
class="h-full w-full"
|
||||
/>
|
||||
</div>
|
||||
@@ -180,11 +202,11 @@ defineExpose({
|
||||
>
|
||||
<UploadIcon aria-hidden="true" />
|
||||
</FileInput>
|
||||
<div class="flex items-center gap-2 mx-auto">
|
||||
<div class="flex items-center gap-2 mx-auto" v-if="fileName || mode === 'new'">
|
||||
<InfoIcon v-if="!fileName" aria-hidden="true" class="text-brand-blue" />
|
||||
<CheckCircleIcon v-else aria-hidden="true" class="text-brand-green" />
|
||||
<small class="max-w-64 truncate block" v-tooltip="fileName ?? currentSkin.texture ?? undefined">
|
||||
{{ fileName || (mode === 'edit' ? currentSkin.texture : 'No skin has been uploaded yet.') }}
|
||||
<small class="max-w-64 truncate block" v-tooltip="fileName ?? undefined">
|
||||
{{ fileName || 'No skin has been uploaded yet.' }}
|
||||
</small>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -211,7 +233,7 @@ defineExpose({
|
||||
:selected="selectedCape?.id === cape.id"
|
||||
@select="selectCape(cape)"
|
||||
/>
|
||||
<CapeLikeTextButton tooltip="View more capes" v-if="capes?.length > 3">
|
||||
<CapeLikeTextButton tooltip="View more capes" v-if="(capes?.length || 0) > 3">
|
||||
<template #icon>
|
||||
<ChevronRightIcon />
|
||||
</template>
|
||||
@@ -221,5 +243,24 @@ defineExpose({
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 mt-6">
|
||||
<ButtonStyled color="brand" :disabled="disableSave">
|
||||
<Button @click="save" v-tooltip="disableSave && 'You need to upload a skin first!'" :disabled="disableSave">
|
||||
<CheckIcon v-if="mode === 'new'"/>
|
||||
<SaveIcon v-else />
|
||||
{{ (mode === 'edit' ? 'Save skin' : 'Add skin') }}
|
||||
</Button>
|
||||
</ButtonStyled>
|
||||
<Button @click="hide">
|
||||
<XIcon />
|
||||
Cancel
|
||||
</Button>
|
||||
<ButtonStyled color="red" v-if="mode === 'edit'">
|
||||
<Button @click="handleDelete">
|
||||
<TrashIcon />
|
||||
Delete
|
||||
</Button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</ModalWrapper>
|
||||
</template>
|
||||
|
||||
@@ -74,7 +74,7 @@ defineExpose({
|
||||
<div>
|
||||
<ScrollablePanel class="h-[20rem] w-[30rem]">
|
||||
<div class="grid grid-cols-6 gap-2 items-start w-full">
|
||||
<CapeButton v-for="cape in capes" :key="`cape-${cape.id}`" :cape="cape" :selected="currentCape?.id === cape.id" @select="currentCape = cape" />
|
||||
<CapeButton v-for="cape in capes" :key="cape.id" :name="cape.name" :texture="cape.texture" :selected="currentCape?.id === cape.id" @select="currentCape = cape" />
|
||||
</div>
|
||||
</ScrollablePanel>
|
||||
</div>
|
||||
|
||||
@@ -121,14 +121,14 @@ export async function get_available_skins(): Promise<Skin[]> {
|
||||
}
|
||||
|
||||
export async function add_and_equip_custom_skin(
|
||||
texture_blob: Uint8Array,
|
||||
textureBlob: Uint8Array,
|
||||
variant: SkinModel,
|
||||
cape_override?: Cape,
|
||||
capeOverride?: Cape,
|
||||
): Promise<void> {
|
||||
await invoke('plugin:minecraft-skins|add_and_equip_custom_skin', {
|
||||
texture_blob,
|
||||
textureBlob,
|
||||
variant,
|
||||
cape_override,
|
||||
capeOverride,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
get_available_skins,
|
||||
get_available_capes,
|
||||
filterSavedSkins,
|
||||
filterDefaultSkins, equip_skin,
|
||||
filterDefaultSkins, equip_skin, remove_custom_skin,
|
||||
} from '@/helpers/skins.ts'
|
||||
import { get as getSettings } from '@/helpers/settings.ts'
|
||||
import { get as getCreds } from '@/helpers/mr_auth'
|
||||
@@ -59,13 +59,41 @@ async function loadSkins() {
|
||||
|
||||
async function changeSkin(newSkin: Skin) {
|
||||
selectedSkin.value = newSkin;
|
||||
// TODO: Backend is broken! Enums aren't being serialized/deserialized correctly.
|
||||
await equip_skin(selectedSkin.value).catch(handleError);
|
||||
}
|
||||
|
||||
async function handleSkinSaved(newSkin: Skin | null, oldSkin: Skin | null) {
|
||||
if (oldSkin) {
|
||||
await remove_custom_skin(oldSkin).catch(handleError)
|
||||
skins.value = skins.value.filter(s => s.texture_key !== oldSkin.texture_key)
|
||||
}
|
||||
|
||||
if (newSkin) skins.value.push(newSkin)
|
||||
selectedSkin.value =
|
||||
skins.value.find(s => s.texture_key === newSkin?.texture_key) ??
|
||||
skins.value.find(s => s.is_equipped) ??
|
||||
skins.value[0] ??
|
||||
null
|
||||
}
|
||||
|
||||
async function handleSkinDeleted(deletedSkin: Skin) {
|
||||
skins.value = skins.value.filter(s => s.texture_key !== deletedSkin.texture_key)
|
||||
if (selectedSkin.value?.texture_key === deletedSkin.texture_key) {
|
||||
selectedSkin.value =
|
||||
skins.value.find(s => s.is_equipped) ??
|
||||
skins.value[0] ??
|
||||
null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EditSkinModal ref="editSkinModal" :capes="capes"/>
|
||||
<EditSkinModal
|
||||
ref="editSkinModal"
|
||||
:capes="capes"
|
||||
@saved="handleSkinSaved"
|
||||
@deleted="handleSkinDeleted"
|
||||
/>
|
||||
<SelectCapeModal ref="selectCapeModal" :capes="capes"/>
|
||||
<div class="p-6 grid grid-cols-[300px_1fr] xl:grid-cols-[3fr_5fr] gap-6">
|
||||
<div class="sticky top-6 self-start">
|
||||
|
||||
@@ -101,6 +101,8 @@ export { default as ModrinthServersPurchaseModal } from './billing/ModrinthServe
|
||||
|
||||
// Skins
|
||||
export { default as SkinPreviewRenderer } from "./skin/SkinPreviewRenderer.vue"
|
||||
export { default as CapeButton } from "./skin/CapeButton.vue"
|
||||
export { default as CapeLikeTextButton } from "./skin/CapeLikeTextButton.vue"
|
||||
|
||||
// Version
|
||||
export { default as VersionChannelIndicator } from './version/VersionChannelIndicator.vue'
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
Drag to rotate
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="nametag" class="absolute top-[10%] left-1/2 transform -translate-x-1/2 px-3 py-1 rounded-md text-[225%] pointer-events-none z-10 font-minecraft text-secondary bg-bg-raised shadow-md border">
|
||||
<div v-if="nametag" class="absolute top-[13%] left-1/2 transform -translate-x-1/2 px-3 py-1 rounded-md text-[200%] pointer-events-none z-10 font-minecraft text-white bg-black/60 shadow-md border-solid border-1 border-white/10">
|
||||
{{ nametag }}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user