polish: grid tweaks + start on upload skin modal
This commit is contained in:
committed by
Alejandro González
parent
5d7df183b4
commit
673b9733b3
@@ -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'
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user