fix: lint issues

This commit is contained in:
Calum H.
2025-05-22 23:20:23 +01:00
committed by Alejandro González
parent b3bc3a8731
commit 187e892c18
12 changed files with 319 additions and 213 deletions

View File

@@ -44,7 +44,13 @@
</div>
<div v-else class="logged-out account">
<h4>Not signed in</h4>
<Button v-tooltip="'Log in'" :disabled="loginDisabled" icon-only color="primary" @click="login()">
<Button
v-tooltip="'Log in'"
:disabled="loginDisabled"
icon-only
color="primary"
@click="login()"
>
<LogInIcon v-if="!loginDisabled" />
<SpinnerIcon v-else class="animate-spin" />
</Button>
@@ -95,7 +101,7 @@ defineProps({
const emit = defineEmits(['change'])
const accounts = ref({})
const loginDisabled = ref(false);
const loginDisabled = ref(false)
const defaultUser = ref()
async function refreshValues() {
@@ -104,13 +110,13 @@ async function refreshValues() {
}
function setLoginDisabled(value) {
loginDisabled.value = value;
loginDisabled.value = value
}
defineExpose({
refreshValues,
setLoginDisabled,
loginDisabled
loginDisabled,
})
await refreshValues()
@@ -129,7 +135,7 @@ async function setAccount(account) {
}
async function login() {
loginDisabled.value = true;
loginDisabled.value = true
const loggedIn = await login_flow().catch(handleSevereError)
if (loggedIn) {
@@ -138,7 +144,7 @@ async function login() {
}
trackEvent('AccountLogIn')
loginDisabled.value = false;
loginDisabled.value = false
}
const logout = async (id) => {

View File

@@ -16,7 +16,8 @@
:variant="variant"
:texture-src="previewSkin"
:cape-src="selectedCapeTexture"
:scale="1.4" :fov="50"
:scale="1.4"
:fov="50"
:initial-rotation="Math.PI / 8"
class="h-full w-full"
/>
@@ -26,14 +27,12 @@
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
<section>
<h2 class="text-base font-semibold mb-2">Texture</h2>
<Button @click="openUploadSkinModal">
<UploadIcon /> Replace texture
</Button>
<Button @click="openUploadSkinModal"> <UploadIcon /> Replace texture </Button>
</section>
<section>
<h2 class="text-base font-semibold mb-2">Arm style</h2>
<RadioButtons v-model="variant" :items="['CLASSIC','SLIM']">
<RadioButtons v-model="variant" :items="['CLASSIC', 'SLIM']">
<template #default="{ item }">
{{ item === 'CLASSIC' ? 'Wide' : 'Slim' }}
</template>
@@ -69,7 +68,7 @@
tooltip="View more capes"
@mouseup="openSelectCapeModal"
>
<template #icon><ChevronRightIcon/></template>
<template #icon><ChevronRightIcon /></template>
<span>More</span>
</CapeLikeTextButton>
</div>
@@ -84,41 +83,49 @@
{{ mode === 'new' ? 'Add skin' : 'Save skin' }}
</Button>
</ButtonStyled>
<Button @click="hide"><XIcon/>Cancel</Button>
<Button @click="hide"><XIcon />Cancel</Button>
</div>
</NewModal>
<SelectCapeModal ref="selectCapeModal" :capes="capes || []" @select="handleCapeSelected" @cancel="handleCapeCancel" />
<SelectCapeModal
ref="selectCapeModal"
:capes="capes || []"
@select="handleCapeSelected"
@cancel="handleCapeCancel"
/>
</template>
<script setup lang="ts">
import { ref, computed, watch, useTemplateRef } from 'vue'
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
import {
Card, FileInput, SkinPreviewRenderer,
Button, RadioButtons, CapeButton, CapeLikeTextButton, ButtonStyled,
NewModal
SkinPreviewRenderer,
Button,
RadioButtons,
CapeButton,
CapeLikeTextButton,
ButtonStyled,
NewModal,
} from '@modrinth/ui'
import {
add_and_equip_custom_skin,
remove_custom_skin,
unequip_skin,
type Skin, type Cape, type SkinModel
type Skin,
type Cape,
type SkinModel,
} from '@/helpers/skins.ts'
import { handleError } from '@/store/notifications'
import {
UploadIcon,
CheckIcon, SaveIcon, XIcon, TrashIcon, ChevronRightIcon
} from '@modrinth/assets'
import { UploadIcon, CheckIcon, SaveIcon, XIcon, ChevronRightIcon } from '@modrinth/assets'
const modal = useTemplateRef('modal')
const selectCapeModal = useTemplateRef('selectCapeModal')
const mode = ref<'new'|'edit'>('new')
const currentSkin = ref<Skin|null>(null)
const mode = ref<'new' | 'edit'>('new')
const currentSkin = ref<Skin | null>(null)
const shouldRestoreModal = ref(false)
const fileUploadTextureBlob = ref<Uint8Array|null>(null)
const fileName = ref<string|null>(null)
const fileUploadTextureBlob = ref<Uint8Array | null>(null)
const fileName = ref<string | null>(null)
watch(fileUploadTextureBlob, () => {
if (fileName.value === null && fileUploadTextureBlob.value) {
fileName.value = 'New upload'
@@ -126,7 +133,7 @@ watch(fileUploadTextureBlob, () => {
})
const variant = ref<SkinModel>('CLASSIC')
const selectedCape = ref<Cape|undefined>(undefined)
const selectedCape = ref<Cape | undefined>(undefined)
const props = defineProps<{ capes?: Cape[] }>()
const selectedCapeTexture = computed(() => selectedCape.value?.texture)
@@ -134,11 +141,11 @@ const visibleCapeList = ref<Cape[]>([])
const sortedCapes = computed(() => {
return [...(props.capes || [])].sort((a, b) => {
const nameA = (a.name || '').toLowerCase();
const nameB = (b.name || '').toLowerCase();
return nameA.localeCompare(nameB);
});
});
const nameA = (a.name || '').toLowerCase()
const nameB = (b.name || '').toLowerCase()
return nameA.localeCompare(nameB)
})
})
function initVisibleCapeList() {
if (!props.capes || props.capes.length === 0) {
@@ -163,10 +170,10 @@ function getSortedCapes(count: number): Cape[] {
function getSortedCapeExcluding(excludeId: string): Cape | undefined {
if (!sortedCapes.value || sortedCapes.value.length <= 1) return undefined
return sortedCapes.value.find(cape => cape.id !== excludeId)
return sortedCapes.value.find((cape) => cape.id !== excludeId)
}
const localPreviewUrl = ref<string|null>(null)
const localPreviewUrl = ref<string | null>(null)
watch(fileUploadTextureBlob, (blob, prev) => {
if (prev && localPreviewUrl.value) URL.revokeObjectURL(localPreviewUrl.value)
if (blob) localPreviewUrl.value = URL.createObjectURL(new Blob([blob]))
@@ -179,24 +186,25 @@ const previewSkin = computed(() => {
})
const hasEdits = computed(() => {
if (mode.value !== 'edit') return true;
if (fileUploadTextureBlob.value) return true;
if (!currentSkin.value) return false;
if (variant.value !== currentSkin.value.variant) return true;
if ((selectedCape.value?.id || null) !== (currentSkin.value.cape_id || null)) return true;
return false;
});
if (mode.value !== 'edit') return true
if (fileUploadTextureBlob.value) return true
if (!currentSkin.value) return false
if (variant.value !== currentSkin.value.variant) return true
if ((selectedCape.value?.id || null) !== (currentSkin.value.cape_id || null)) return true
return false
})
const disableSave = computed(() =>
(mode.value === 'new' && !fileUploadTextureBlob.value) ||
(mode.value === 'edit' && !hasEdits.value)
);
const disableSave = computed(
() =>
(mode.value === 'new' && !fileUploadTextureBlob.value) ||
(mode.value === 'edit' && !hasEdits.value),
)
const saveTooltip = computed(() => {
if (mode.value === 'new' && !fileUploadTextureBlob.value) return 'Upload a skin first!';
if (mode.value === 'edit' && !hasEdits.value) return 'Make an edit to the skin first!';
return undefined;
});
if (mode.value === 'new' && !fileUploadTextureBlob.value) return 'Upload a skin first!'
if (mode.value === 'edit' && !hasEdits.value) return 'Make an edit to the skin first!'
return undefined
})
function resetState() {
mode.value = 'new'
@@ -218,7 +226,7 @@ function show(e: MouseEvent, skin?: Skin) {
currentSkin.value = skin ?? null
if (skin) {
variant.value = skin.variant
selectedCape.value = props.capes?.find(c=>c.id===skin.cape_id)
selectedCape.value = props.capes?.find((c) => c.id === skin.cape_id)
} else {
variant.value = 'CLASSIC'
selectedCape.value = undefined
@@ -243,7 +251,7 @@ function showNew(e: MouseEvent, skinTexture: Uint8Array, filename: string) {
function restoreWithNewTexture(skinTexture: Uint8Array, filename: string) {
fileUploadTextureBlob.value = skinTexture
fileName.value = filename
if (shouldRestoreModal.value) {
setTimeout(() => {
modal.value?.show()
@@ -257,12 +265,12 @@ function hide() {
setTimeout(() => resetState(), 250)
}
function selectCape(cape: Cape|undefined) {
function selectCape(cape: Cape | undefined) {
if (cape && selectedCape.value?.id !== cape.id) {
const isInVisibleList = visibleCapeList.value.some(c => c.id === cape.id)
const isInVisibleList = visibleCapeList.value.some((c) => c.id === cape.id)
if (!isInVisibleList && visibleCapeList.value.length > 0) {
visibleCapeList.value.splice(0, 1, cape)
if (visibleCapeList.value.length > 1 && visibleCapeList.value[1].id === cape.id) {
const otherCape = getSortedCapeExcluding(cape.id)
if (otherCape) {
@@ -306,7 +314,7 @@ function openSelectCapeModal(e: MouseEvent) {
currentSkin.value?.texture_key,
selectedCape.value,
previewSkin.value,
variant.value
variant.value,
)
}, 0)
}
@@ -329,7 +337,7 @@ async function save() {
blob = new Uint8Array(buf)
}
await unequip_skin();
await unequip_skin()
if (mode.value === 'new') {
await add_and_equip_custom_skin(blob, variant.value, selectedCape.value)
@@ -346,9 +354,13 @@ async function save() {
}
}
watch(() => props.capes, () => {
initVisibleCapeList()
}, { immediate: true })
watch(
() => props.capes,
() => {
initVisibleCapeList()
},
{ immediate: true },
)
const emit = defineEmits<{
(event: 'saved'): void
@@ -361,6 +373,6 @@ defineExpose({
showNew,
restoreWithNewTexture,
hide,
shouldRestoreModal
shouldRestoreModal,
})
</script>

View File

@@ -7,7 +7,7 @@ import {
CapeButton,
CapeLikeTextButton,
SkinPreviewRenderer,
NewModal
NewModal,
} from '@modrinth/ui'
import { CheckIcon, XIcon } from '@modrinth/assets'
@@ -24,11 +24,11 @@ const props = defineProps<{
const sortedCapes = computed(() => {
return [...props.capes].sort((a, b) => {
const nameA = (a.name || '').toLowerCase();
const nameB = (b.name || '').toLowerCase();
return nameA.localeCompare(nameB);
});
});
const nameA = (a.name || '').toLowerCase()
const nameB = (b.name || '').toLowerCase()
return nameA.localeCompare(nameB)
})
})
const currentSkinId = ref<string | undefined>()
const currentSkinTexture = ref<string | undefined>()
@@ -93,7 +93,7 @@ defineExpose({
:variant="currentSkinVariant"
:scale="1.4"
:fov="50"
:initial-rotation="Math.PI + (Math.PI / 8)"
:initial-rotation="Math.PI + Math.PI / 8"
class="h-full w-full"
/>
</div>

View File

@@ -1,9 +1,7 @@
<template>
<NewModal ref="modal" @on-hide="hide(true)">
<template #title>
<span class="text-lg font-extrabold text-contrast">
Upload skin texture
</span>
<span class="text-lg font-extrabold text-contrast"> Upload skin texture </span>
</template>
<div class="relative">
<div
@@ -12,10 +10,22 @@
@drop.prevent="handleFileOperation"
@dragover.prevent
>
<p class="mx-auto mb-0 text-primary text-xl text-center flex items-center gap-2"><UploadIcon /> Select skin texture file</p>
<p class="mx-auto mt-0 text-secondary text-sm text-center">Drag and drop or click here to browse</p>
<p class="mx-auto mt-0 text-secondary text-xs text-center">Only 64x64 PNG files are accepted</p>
<input ref="fileInput" type="file" accept="image/png" class="hidden" @change="handleFileOperation" />
<p class="mx-auto mb-0 text-primary text-xl text-center flex items-center gap-2">
<UploadIcon /> Select skin texture file
</p>
<p class="mx-auto mt-0 text-secondary text-sm text-center">
Drag and drop or click here to browse
</p>
<p class="mx-auto mt-0 text-secondary text-xs text-center">
Only 64x64 PNG files are accepted
</p>
<input
ref="fileInput"
type="file"
accept="image/png"
class="hidden"
@change="handleFileOperation"
/>
</div>
</div>
</NewModal>
@@ -24,7 +34,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { UploadIcon } from '@modrinth/assets'
import { useNotifications } from "@/store/state"
import { useNotifications } from '@/store/state'
import { NewModal } from '@modrinth/ui'
const notifications = useNotifications()
@@ -71,30 +81,30 @@ 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;
const files = (e as DragEvent).dataTransfer?.files || (e.target as HTMLInputElement).files
if (!files || files.length === 0) {
return;
return
}
const file = files[0];
const file = files[0]
if (file.type !== 'image/png') {
notifications.addNotification({
title: 'Invalid file type.',
text: 'Only PNG files are accepted.',
type: 'error',
});
return;
})
return
}
const isValidDimensions = await validateImageDimensions(file);
const isValidDimensions = await validateImageDimensions(file)
if (!isValidDimensions) {
notifications.addNotification({
title: 'Invalid dimensions.',
text: 'Only 64x64 PNG files are accepted.',
type: 'error',
});
return;
})
return
}
emit('uploaded', file);
hide();
emit('uploaded', file)
hide()
}
defineExpose({ show, hide })

View File

@@ -1,14 +1,22 @@
<script setup lang="ts">
import { UpdatedIcon, PlusIcon, ExcitedRinthbot, LogInIcon, SpinnerIcon, EditIcon, TrashIcon } from '@modrinth/assets'
import {
UpdatedIcon,
PlusIcon,
ExcitedRinthbot,
LogInIcon,
SpinnerIcon,
EditIcon,
TrashIcon,
} from '@modrinth/assets'
import {
ButtonStyled,
SkinPreviewRenderer,
SkinButton,
SkinLikeTextButton,
Button,
ConfirmModal
ConfirmModal,
} from '@modrinth/ui'
import type { Ref } from 'vue';
import type { Ref } from 'vue'
import { ref, computed, useTemplateRef, watch, onMounted, onUnmounted, inject } from 'vue'
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
@@ -28,9 +36,9 @@ 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/batchSkinRenderer.ts'
import { generateSkinPreviews, map } from '@/helpers/rendering/batchSkinRenderer.ts'
import { handleSevereError } from "@/store/error";
import { trackEvent } from "@/helpers/analytics";
import type AccountsCard from "@/components/ui/AccountsCard.vue";
import { handleSevereError } from '@/store/error'
import { trackEvent } from '@/helpers/analytics'
import type AccountsCard from '@/components/ui/AccountsCard.vue'
const editSkinModal = useTemplateRef('editSkinModal')
const selectCapeModal = useTemplateRef('selectCapeModal')
@@ -64,7 +72,9 @@ const currentCape = computed(() => {
const skinTexture = computed(() => selectedSkin.value?.texture ?? '')
const capeTexture = computed(() => currentCape.value?.texture)
const skinVariant = computed(() => selectedSkin.value?.variant)
const skinNametag = computed(() => settings.value.hide_nametag_skins_page ? undefined : username.value)
const skinNametag = computed(() =>
settings.value.hide_nametag_skins_page ? undefined : username.value,
)
let userCheckInterval: number | null = null
@@ -141,7 +151,7 @@ function getBakedSkinTextures(skin: Skin): RenderResult | undefined {
}
async function login() {
accountsCard.value.setLoginDisabled(true);
accountsCard.value.setLoginDisabled(true)
const loggedIn = await login_flow().catch(handleSevereError)
if (loggedIn && accountsCard) {
@@ -149,7 +159,7 @@ async function login() {
}
trackEvent('AccountLogIn')
accountsCard.value.setLoginDisabled(false);
accountsCard.value.setLoginDisabled(false)
}
function openUploadSkinModal(e: MouseEvent) {
@@ -158,9 +168,9 @@ function openUploadSkinModal(e: MouseEvent) {
function onSkinFileUploaded(file: File) {
const fakeEvent = new MouseEvent('click')
file.arrayBuffer().then(buf => {
file.arrayBuffer().then((buf) => {
const skinTexture = new Uint8Array(buf)
if (editSkinModal.value && editSkinModal.value.shouldRestoreModal) {
editSkinModal.value.restoreWithNewTexture(skinTexture, file.name)
} else {
@@ -181,16 +191,16 @@ function onUploadCanceled() {
watch(
() => selectedSkin.value?.cape_id,
() => {}
() => {},
)
onMounted(() => {
userCheckInterval = window.setInterval(checkUserChanges, 250);
userCheckInterval = window.setInterval(checkUserChanges, 250)
})
onUnmounted(() => {
if (userCheckInterval !== null) {
window.clearInterval(userCheckInterval);
window.clearInterval(userCheckInterval)
}
})
@@ -198,9 +208,9 @@ async function checkUserChanges() {
try {
const defaultId = await get_default_user()
if (defaultId !== currentUserId.value) {
await loadCurrentUser();
await loadCapes();
await loadSkins();
await loadCurrentUser()
await loadCapes()
await loadSkins()
}
} catch (error) {
if (currentUser.value) {
@@ -221,7 +231,11 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
@open-upload-modal="openUploadSkinModal"
/>
<SelectCapeModal ref="selectCapeModal" :capes="capes" @select="handleCapeSelected" />
<UploadSkinModal ref="uploadSkinModal" @uploaded="onSkinFileUploaded" @canceled="onUploadCanceled" />
<UploadSkinModal
ref="uploadSkinModal"
@uploaded="onSkinFileUploaded"
@canceled="onUploadCanceled"
/>
<ConfirmModal
ref="deleteSkinModal"
title="Are you sure you want to delete this skin?"
@@ -237,20 +251,21 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
<ButtonStyled :disabled="!!selectedSkin?.cape_id">
<button
v-tooltip="
selectedSkin?.cape_id
? 'The equipped skin is overriding the default cape.'
: undefined
"
selectedSkin?.cape_id
? 'The equipped skin is overriding the default cape.'
: undefined
"
:disabled="!!selectedSkin?.cape_id"
@click="
(e: MouseEvent) => selectCapeModal?.show(
e,
selectedSkin?.texture_key,
currentCape,
skinTexture,
skinVariant
)
"
(e: MouseEvent) =>
selectCapeModal?.show(
e,
selectedSkin?.texture_key,
currentCape,
skinTexture,
skinVariant,
)
"
>
<UpdatedIcon />
Change cape
@@ -275,15 +290,11 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
<section class="flex flex-col gap-2 mt-1">
<h2 class="text-lg font-bold m-0 text-primary">Saved skins</h2>
<div class="skin-card-grid">
<SkinLikeTextButton
class="skin-card"
tooltip="Add a skin"
@click="openUploadSkinModal"
>
<template #icon>
<PlusIcon class="w-5 h-5 stroke-2" />
</template>
<span>Add a skin</span>
<SkinLikeTextButton class="skin-card" tooltip="Add a skin" @click="openUploadSkinModal">
<template #icon>
<PlusIcon class="w-5 h-5 stroke-2" />
</template>
<span>Add a skin</span>
</SkinLikeTextButton>
<SkinButton
@@ -296,24 +307,24 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
@select="changeSkin(skin)"
>
<template #overlay-buttons>
<Button
color="green"
aria-label="Edit skin"
@click.stop="(e) => editSkinModal?.show(e, skin)"
>
<EditIcon class="w-5 h-5" /> Edit
</Button>
<Button
v-show="!skin.is_equipped"
v-tooltip="'Delete skin'"
aria-label="Delete skin"
color="red"
class="!rounded-[100%]"
icon-only
@click.stop="() => confirmDeleteSkin(skin)"
>
<TrashIcon class="w-5 h-5" />
</Button>
<Button
color="green"
aria-label="Edit skin"
@click.stop="(e) => editSkinModal?.show(e, skin)"
>
<EditIcon class="w-5 h-5" /> Edit
</Button>
<Button
v-show="!skin.is_equipped"
v-tooltip="'Delete skin'"
aria-label="Delete skin"
color="red"
class="!rounded-[100%]"
icon-only
@click.stop="() => confirmDeleteSkin(skin)"
>
<TrashIcon class="w-5 h-5" />
</Button>
</template>
</SkinButton>
</div>
@@ -338,14 +349,32 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
</div>
<div v-else class="flex items-center justify-center min-h-[50vh] pt-[25%]">
<div class="bg-bg-raised rounded-lg p-7 flex flex-col gap-5 shadow-md relative max-w-xl w-full mx-auto">
<img :src="ExcitedRinthbot" alt="Excited Modrinth Bot" class="absolute -top-28 right-8 md:right-20 h-28 w-auto" />
<div class="absolute top-0 left-0 w-full h-[1px] opacity-40 bg-gradient-to-r from-transparent via-green-500 to-transparent" style="background: linear-gradient(to right, transparent 2rem, var(--color-green) calc(100% - 13rem), var(--color-green) calc(100% - 5rem), transparent calc(100% - 2rem))"></div>
<div
class="bg-bg-raised rounded-lg p-7 flex flex-col gap-5 shadow-md relative max-w-xl w-full mx-auto"
>
<img
:src="ExcitedRinthbot"
alt="Excited Modrinth Bot"
class="absolute -top-28 right-8 md:right-20 h-28 w-auto"
/>
<div
class="absolute top-0 left-0 w-full h-[1px] opacity-40 bg-gradient-to-r from-transparent via-green-500 to-transparent"
style="
background: linear-gradient(
to right,
transparent 2rem,
var(--color-green) calc(100% - 13rem),
var(--color-green) calc(100% - 5rem),
transparent calc(100% - 2rem)
);
"
></div>
<div class="flex flex-col gap-5">
<h1 class="text-3xl font-extrabold m-0">Please sign-in</h1>
<p class="text-lg m-0">
Please sign into your Minecraft account to use the skin management features of the Modrinth app.
Please sign into your Minecraft account to use the skin management features of the
Modrinth app.
</p>
<ButtonStyled v-show="accountsCard" color="brand" :disabled="accountsCard.loginDisabled">
<Button :disabled="accountsCard.loginDisabled" @click="login">

View File

@@ -66,13 +66,13 @@ function onScroll({ target: { scrollTop, offsetHeight, scrollHeight } }) {
<style lang="scss" scoped>
@property --_top-fade-height {
syntax: "<length-percentage>";
syntax: '<length-percentage>';
inherits: false;
initial-value: 0%;
}
@property --_bottom-fade-height {
syntax: "<length-percentage>";
syntax: '<length-percentage>';
inherits: false;
initial-value: 0%;
}
@@ -88,15 +88,17 @@ function onScroll({ target: { scrollTop, offsetHeight, scrollHeight } }) {
display: flex;
overflow: hidden;
position: relative;
transition: --_top-fade-height 0.05s linear, --_bottom-fade-height 0.05s linear;
transition:
--_top-fade-height 0.05s linear,
--_bottom-fade-height 0.05s linear;
--_fade-height: 3rem;
mask-image: linear-gradient(
transparent,
rgb(0 0 0 / 100%) var(--_top-fade-height, 0%),
rgb(0 0 0 / 100%) calc(100% - var(--_bottom-fade-height, 0%)),
transparent 100%
transparent,
rgb(0 0 0 / 100%) var(--_top-fade-height, 0%),
rgb(0 0 0 / 100%) calc(100% - var(--_bottom-fade-height, 0%)),
transparent 100%
);
&.top-fade {

View File

@@ -100,11 +100,11 @@ export { default as AddPaymentMethodModal } from './billing/AddPaymentMethodModa
export { default as ModrinthServersPurchaseModal } from './billing/ModrinthServersPurchaseModal.vue'
// 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"
export { default as SkinButton } from "./skin/SkinButton.vue"
export { default as SkinLikeTextButton } from "./skin/SkinLikeTextButton.vue"
export { default as SkinPreviewRenderer } from './skin/SkinPreviewRenderer.vue'
export { default as CapeButton } from './skin/CapeButton.vue'
export { default as CapeLikeTextButton } from './skin/CapeLikeTextButton.vue'
export { default as SkinButton } from './skin/SkinButton.vue'
export { default as SkinLikeTextButton } from './skin/SkinLikeTextButton.vue'
// Version
export { default as VersionChannelIndicator } from './version/VersionChannelIndicator.vue'

View File

@@ -19,13 +19,18 @@ const props = withDefaults(
},
)
console.log(props);
console.log(props)
const highlighted = computed(() => props.selected ?? props.isEquipped)
</script>
<template>
<button v-tooltip="name" class="block border-0 m-0 p-0 bg-transparent group cursor-pointer" :aria-label="name" @click="emit('select')">
<button
v-tooltip="name"
class="block border-0 m-0 p-0 bg-transparent group cursor-pointer"
:aria-label="name"
@click="emit('select')"
>
<span
:class="
highlighted

View File

@@ -26,7 +26,7 @@ withDefaults(
'block rounded-lg group-active:scale-95 transition-all border-2 relative',
highlighted
? 'border-brand highlighted-glow'
: 'border-transparent opacity-75 group-hover:opacity-100'
: 'border-transparent opacity-75 group-hover:opacity-100',
]"
>
<span class="block p-[3px] rounded-lg bg-button-bg">

View File

@@ -6,20 +6,23 @@ const emit = defineEmits<{
(e: 'edit', event: MouseEvent): void
}>()
const props = withDefaults(defineProps<{
forwardImageSrc?: string
backwardImageSrc?: string
selected: boolean
tooltip?: string
}>(), {
forwardImageSrc: undefined,
backwardImageSrc: undefined,
tooltip: undefined,
})
const props = withDefaults(
defineProps<{
forwardImageSrc?: string
backwardImageSrc?: string
selected: boolean
tooltip?: string
}>(),
{
forwardImageSrc: undefined,
backwardImageSrc: undefined,
tooltip: undefined,
},
)
const imagesLoaded = ref({
forward: Boolean(props.forwardImageSrc),
backward: Boolean(props.backwardImageSrc)
backward: Boolean(props.backwardImageSrc),
})
function onImageLoad(type: 'forward' | 'backward') {
@@ -31,9 +34,7 @@ function onImageLoad(type: 'forward' | 'backward') {
<div
v-tooltip="tooltip ?? undefined"
class="group flex relative overflow-hidden rounded-xl border-solid border-2 transition-colors duration-200"
:class="[
selected ? 'border-brand' : 'border-transparent hover:border-white/50'
]"
:class="[selected ? 'border-brand' : 'border-transparent hover:border-white/50']"
>
<button
class="skin-btn-bg absolute inset-0 cursor-pointer p-0 border-none group-hover:brightness-125"
@@ -41,13 +42,19 @@ function onImageLoad(type: 'forward' | 'backward') {
@click="emit('select')"
></button>
<div v-if="!(imagesLoaded.forward && imagesLoaded.backward)" class="skeleton-loader w-full h-full">
<div
v-if="!(imagesLoaded.forward && imagesLoaded.backward)"
class="skeleton-loader w-full h-full"
>
<div class="skeleton absolute inset-0 aspect-[5/7]"></div>
</div>
<span
v-show="imagesLoaded.forward && imagesLoaded.backward"
:class="['skin-button__image-parent pointer-events-none w-full h-full grid [transform-style:preserve-3d] transition-transform duration-500 group-hover:[transform:rotateY(180deg)] place-items-stretch', selected ? 'with-shadow' : '']"
:class="[
'skin-button__image-parent pointer-events-none w-full h-full grid [transform-style:preserve-3d] transition-transform duration-500 group-hover:[transform:rotateY(180deg)] place-items-stretch',
selected ? 'with-shadow' : '',
]"
>
<img
alt=""
@@ -68,7 +75,7 @@ function onImageLoad(type: 'forward' | 'backward') {
<span
v-if="$slots['overlay-buttons']"
class="absolute inset-0 flex items-end justify-start p-1 gap-1 translate-y-4 scale-75 opacity-0 transition-all group-hover:opacity-100 group-hover:scale-100 group-hover:translate-y-0 group-hover:translate-x-0"
style="pointer-events: none;"
style="pointer-events: none"
>
<slot name="overlay-buttons" />
</span>
@@ -104,7 +111,13 @@ function onImageLoad(type: 'forward' | 'backward') {
background: linear-gradient(180deg, #3a3d47 0%, #33363d 100%);
}
.skin-btn-bg.selected {
background: linear-gradient(157.61deg, var(--color-brand) -76.68%, rgba(27, 217, 106, 0.534) -38.61%, rgba(12, 89, 44, 0.6) 100.4%), #27292F;
background: linear-gradient(
157.61deg,
var(--color-brand) -76.68%,
rgba(27, 217, 106, 0.534) -38.61%,
rgba(12, 89, 44, 0.6) 100.4%
),
#27292f;
}
.skin-btn-bg.selected:hover,

View File

@@ -14,7 +14,9 @@ const pressed = ref(false)
@mouseup="pressed = false"
@mouseleave="pressed = false"
></button>
<div class="relative w-full h-full flex flex-col items-center justify-center pointer-events-none">
<div
class="relative w-full h-full flex flex-col items-center justify-center pointer-events-none"
>
<div class="mb-2">
<slot name="icon"></slot>
</div>
@@ -28,7 +30,9 @@ const pressed = ref(false)
<style scoped lang="scss">
.skin-like-text-bg {
background: linear-gradient(180deg, #3a3d47 0%, #33363d 100%);
transition: filter 200ms ease-in-out, box-shadow 200ms ease-in-out;
transition:
filter 200ms ease-in-out,
box-shadow 200ms ease-in-out;
}
.skin-like-text-bg:hover {
filter: brightness(var(--hover-brightness));

View File

@@ -1,11 +1,16 @@
<template>
<div class="relative w-full h-full cursor-grab">
<div class="absolute bottom-[18%] left-0 right-0 flex justify-center items-center mb-2 pointer-events-none z-10">
<div
class="absolute bottom-[18%] left-0 right-0 flex justify-center items-center mb-2 pointer-events-none z-10"
>
<span class="text-primary text-xs px-2 py-1 rounded-full backdrop-blur-sm">
Drag to rotate
</span>
</div>
<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-primary nametag-bg">
<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-primary nametag-bg"
>
{{ nametag }}
</div>
@@ -13,18 +18,22 @@
shadows
alpha
:antialias="antialias"
:renderer-options="{
outputColorSpace: THREE.SRGBColorSpace,
toneMapping: THREE.NoToneMapping,
}"
@pointerdown="onPointerDown"
@pointermove="onPointerMove"
@pointerup="onPointerUp"
@pointerleave="onPointerUp"
:rendererOptions="{
outputColorSpace: THREE.SRGBColorSpace,
toneMapping: THREE.NoToneMapping
}"
>
<Suspense>
<Group>
<Group :rotation="[0, modelRotation, 0]" :position="[0, -0.05 * scale, 1.95]" :scale="[0.8 * scale, 0.8 * scale, 0.8 * scale]">
<Group
:rotation="[0, modelRotation, 0]"
:position="[0, -0.05 * scale, 1.95]"
:scale="[0.8 * scale, 0.8 * scale, 0.8 * scale]"
>
<primitive v-if="scene" :object="scene" />
</Group>
@@ -42,7 +51,11 @@
/>
</TresMesh>
<TresMesh :position="[0, -0.1 * scale, 2]" :rotation="[-Math.PI / 2, 0, 0]" :scale="[0.75 * scale, 0.75 * scale, 0.75 * scale]">
<TresMesh
:position="[0, -0.1 * scale, 2]"
:rotation="[-Math.PI / 2, 0, 0]"
:scale="[0.75 * scale, 0.75 * scale, 0.75 * scale]"
>
<TresPlaneGeometry :args="[2, 2]" />
<TresMeshBasicMaterial
:map="radialTexture"
@@ -55,7 +68,7 @@
</Suspense>
<TresPerspectiveCamera
:makeDefault="true"
:make-default="true"
:fov="fov"
:position="[0, 1.5, -3.25]"
:look-at="target"
@@ -83,7 +96,7 @@ const props = withDefaults(
nametag?: string
antialias?: boolean
scale?: number
fov?: number,
fov?: number
initialRotation?: number
}>(),
{
@@ -94,11 +107,11 @@ const props = withDefaults(
capeModelSrc: '',
capeSrc: undefined,
initialRotation: 15.75,
}
},
)
const selectedModelSrc = computed(() =>
props.variant === 'SLIM' ? props.slimModelSrc : props.wideModelSrc
props.variant === 'SLIM' ? props.slimModelSrc : props.wideModelSrc,
)
const scene = shallowRef<THREE.Object3D | null>(null)
@@ -135,7 +148,7 @@ async function loadModel(src: string) {
}
bodyNode.value = null
loadedScene.traverse(node => {
loadedScene.traverse((node) => {
if (node.name === 'Body') {
bodyNode.value = node
}
@@ -218,11 +231,11 @@ function attachCapeToBody() {
function applyTextureToScene(root: THREE.Object3D | null, tex: THREE.Texture | null) {
if (!root || !tex) return
root.traverse(child => {
root.traverse((child) => {
if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh
if (mesh.name === "Cape") return
if (mesh.name === 'Cape') return
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material]
@@ -242,7 +255,7 @@ function applyTextureToScene(root: THREE.Object3D | null, tex: THREE.Texture | n
function applyCapeTexture(root: THREE.Object3D | null, tex: THREE.Texture | null) {
if (!root) return
root.traverse(child => {
root.traverse((child) => {
if ((child as THREE.Mesh).isMesh) {
const mesh = child as THREE.Mesh
@@ -282,7 +295,7 @@ const isDragging = ref(false)
const previousX = ref(0)
const onPointerDown = (event: PointerEvent) => {
(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)
;(event.currentTarget as HTMLElement).setPointerCapture(event.pointerId)
isDragging.value = true
previousX.value = event.clientX
}
@@ -295,8 +308,8 @@ const onPointerMove = (event: PointerEvent) => {
}
const onPointerUp = (event: PointerEvent) => {
isDragging.value = false;
(event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId)
isDragging.value = false
;(event.currentTarget as HTMLElement).releasePointerCapture(event.pointerId)
}
const radialTexture = createRadialTexture(512)
@@ -316,17 +329,26 @@ function createRadialTexture(size: number): THREE.CanvasTexture {
return new THREE.CanvasTexture(canvas)
}
watch(selectedModelSrc, src => loadModel(src))
watch(() => props.capeModelSrc, src => src && loadCape(src))
watch(() => props.textureSrc, async newSrc => {
texture.value = await loadAndApplyTexture(newSrc)
if (scene.value && texture.value) {
applyTextureToScene(scene.value, texture.value)
}
})
watch(() => props.capeSrc, async newCapeSrc => {
await loadAndApplyCapeTexture(newCapeSrc)
})
watch(selectedModelSrc, (src) => loadModel(src))
watch(
() => props.capeModelSrc,
(src) => src && loadCape(src),
)
watch(
() => props.textureSrc,
async (newSrc) => {
texture.value = await loadAndApplyTexture(newSrc)
if (scene.value && texture.value) {
applyTextureToScene(scene.value, texture.value)
}
},
)
watch(
() => props.capeSrc,
async (newCapeSrc) => {
await loadAndApplyCapeTexture(newCapeSrc)
},
)
onBeforeMount(async () => {
texture.value = await loadAndApplyTexture(props.textureSrc)
@@ -345,7 +367,10 @@ onBeforeMount(async () => {
<style scoped lang="scss">
.nametag-bg {
background: linear-gradient(308.68deg, rgba(0, 0, 0, 0) -52.46%, rgba(100, 100, 100, 0.1) 94.75%), rgba(0, 0, 0, 0.2);
box-shadow: inset -0.5px -0.5px 0px rgba(0, 0, 0, 0.25), inset 0.5px 0.5px 0px rgba(255, 255, 255, 0.05);
background: linear-gradient(308.68deg, rgba(0, 0, 0, 0) -52.46%, rgba(100, 100, 100, 0.1) 94.75%),
rgba(0, 0, 0, 0.2);
box-shadow:
inset -0.5px -0.5px 0px rgba(0, 0, 0, 0.25),
inset 0.5px 0.5px 0px rgba(255, 255, 255, 0.05);
}
</style>