polish: grid tweaks + start on upload skin modal

This commit is contained in:
Calum H.
2025-05-22 21:40:54 +01:00
committed by Alejandro González
parent 5d7df183b4
commit 673b9733b3
4 changed files with 78 additions and 55 deletions

View File

@@ -23,26 +23,12 @@
</div>
</div>
<div class="flex flex-col gap-4 w-full">
<div class="flex flex-col gap-4 w-full min-h-[20rem]">
<section>
<h2 class="text-base font-semibold mb-2">Texture</h2>
<Card class="!bg-bg p-4 flex flex-col items-center gap-4">
<FileInput
:max-size="8000"
accept="image/png"
:prompt="mode === 'edit' ? 'Replace skin' : 'Upload a skin'"
class="btn btn-primary"
@change="onTextureSelected"
>
<UploadIcon aria-hidden="true" />
</FileInput>
<div v-if="fileName || mode === 'new'" class="flex items-center gap-2">
<InfoIcon v-if="!fileName" class="text-brand-blue" />
<CheckCircleIcon v-else class="text-brand-green" />
<small v-tooltip="fileName" class="truncate">{{ fileName || 'No skin uploaded yet.' }}</small>
</div>
</Card>
<Button>
<UploadIcon /> Replace texture
</Button>
</section>
<section>
@@ -91,7 +77,7 @@
</div>
</div>
<div class="flex gap-2 mt-6">
<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 />
@@ -124,7 +110,7 @@ import {
} from '@/helpers/skins.ts'
import { handleError } from '@/store/notifications'
import {
CheckCircleIcon, InfoIcon, UploadIcon,
UploadIcon,
CheckIcon, SaveIcon, XIcon, TrashIcon, ChevronRightIcon
} from '@modrinth/assets'

View File

@@ -1,14 +1,15 @@
<script setup lang="ts">
import {UpdatedIcon, PlusIcon, ExcitedRinthbot, LogInIcon, SpinnerIcon } from '@modrinth/assets'
import { UpdatedIcon, PlusIcon, ExcitedRinthbot, LogInIcon, SpinnerIcon, EditIcon, TrashIcon } from '@modrinth/assets'
import {
ButtonStyled,
SkinPreviewRenderer,
SkinButton,
SkinLikeTextButton,
Button
Button,
ConfirmModal
} from '@modrinth/ui'
import type { Ref} from 'vue';
import {ref, computed, useTemplateRef, watch, onMounted, onUnmounted, inject, nextTick} from 'vue'
import type { Ref } from 'vue';
import { ref, computed, useTemplateRef, watch, onMounted, onUnmounted, inject, nextTick } from 'vue'
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
import { handleError } from '@/store/notifications'
@@ -19,14 +20,15 @@ import {
filterDefaultSkins,
equip_skin,
set_default_cape,
remove_custom_skin,
} 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/batchSkinRenderer.ts'
import { generateSkinPreviews, map } from '@/helpers/rendering/batchSkinRenderer.ts'
import {handleSevereError} from "@/store/error";
import {trackEvent} from "@/helpers/analytics";
import { handleSevereError } from "@/store/error";
import { trackEvent } from "@/helpers/analytics";
import type AccountsCard from "@/components/ui/AccountsCard.vue";
const editSkinModal = useTemplateRef('editSkinModal')
@@ -65,6 +67,21 @@ const skinNametag = computed(() => settings.value.hide_nametag_skins_page ? unde
let userCheckInterval: number | null = null
const deleteSkinModal = ref()
const skinToDelete = ref<Skin | null>(null)
function confirmDeleteSkin(skin: Skin) {
skinToDelete.value = skin
deleteSkinModal.value?.show()
}
async function deleteSkin() {
if (!skinToDelete.value) return
await remove_custom_skin(skinToDelete.value).catch(handleError)
await loadSkins()
skinToDelete.value = null
}
async function loadCapes() {
try {
capes.value = (await get_available_capes()) ?? []
@@ -175,6 +192,13 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
@deleted="() => loadSkins()"
/>
<SelectCapeModal ref="selectCapeModal" :capes="capes" @select="handleCapeSelected" />
<ConfirmModal
ref="deleteSkinModal"
title="Are you sure you want to delete this skin?"
description="This will permanently delete the selected skin. This action cannot be undone."
proceed-label="Delete"
@proceed="deleteSkin"
/>
<div v-if="currentUser" class="p-4 skin-layout">
<div class="preview-panel">
@@ -236,13 +260,35 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
v-for="skin in savedSkins"
:key="`saved-skin-${skin.texture_key}`"
class="skin-card"
editable
:forward-image-src="getBakedSkinTextures(skin)?.forwards"
:backward-image-src="getBakedSkinTextures(skin)?.backwards"
:selected="selectedSkin === skin"
@select="changeSkin(skin)"
@edit="(e) => editSkinModal?.show(e, skin)"
/>
>
<template #overlay-buttons>
<div class="flex gap-2" style="pointer-events: auto;">
<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>
</div>
</template>
</SkinButton>
</div>
</section>
@@ -258,7 +304,6 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
:selected="selectedSkin === skin"
:tooltip="skin.name"
@select="changeSkin(skin)"
@edit="(e) => editSkinModal?.show(e, skin)"
/>
</div>
</section>
@@ -288,7 +333,7 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
</template>
<style lang="scss" scoped>
$skin-card-width: 140px;
$skin-card-width: 155px;
$skin-card-gap: 4px;
.skin-layout {
@@ -329,15 +374,20 @@ $skin-card-gap: 4px;
.skin-card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, $skin-card-width);
grid-template-columns: repeat(6, 1fr);
gap: $skin-card-gap;
width: 100%;
@media (max-width: 1110px) {
grid-template-columns: repeat(3, 1fr);
}
}
.skin-card {
width: $skin-card-width;
aspect-ratio: 1.05;
aspect-ratio: 0.95;
border-radius: 10px;
box-sizing: border-box;
width: 100%;
min-width: 0;
}
</style>

View File

@@ -41,6 +41,7 @@ export default {
green: 'var(--color-green-highlight)',
blue: 'var(--color-blue-highlight)',
purple: 'var(--color-purple-highlight)',
gray: 'var(--color-gray-highlight)',
},
divider: {
DEFAULT: 'var(--color-divider)',

View File

@@ -1,11 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue'
import ButtonStyled from "../base/ButtonStyled.vue"
import { commonMessages } from '../../utils'
import { EditIcon } from '@modrinth/assets'
import { useVIntl } from '@vintl/vintl'
const { formatMessage } = useVIntl()
const emit = defineEmits<{
(e: 'select'): void
@@ -16,10 +10,11 @@ const props = withDefaults(defineProps<{
forwardImageSrc?: string
backwardImageSrc?: string
selected: boolean
editable?: boolean
tooltip?: string
}>(), {
editable: false,
forwardImageSrc: undefined,
backwardImageSrc: undefined,
tooltip: undefined,
})
const imagesLoaded = ref({
@@ -71,20 +66,11 @@ function onImageLoad(type: 'forward' | 'backward') {
</span>
<span
v-if="editable"
class="absolute pointer-events-none inset-0 flex items-end justify-start p-2 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"
v-if="$slots['overlay-buttons']"
class="absolute inset-0 flex items-end justify-start p-2 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;"
>
<ButtonStyled color="brand">
<button
class="pointer-events-auto shadow-black/50 shadow-lg"
@mousedown.stop
@mouseup.stop
@click="(e) => emit('edit', e)"
>
<EditIcon />
{{ formatMessage(commonMessages.editButton) }}
</button>
</ButtonStyled>
<slot name="overlay-buttons" />
</span>
</div>
</template>