fix: polish

This commit is contained in:
Calum H.
2025-06-24 19:08:23 +01:00
parent cbb7c80e21
commit 1aca3e1108
6 changed files with 117 additions and 40 deletions

View File

@@ -58,9 +58,9 @@ watch(
<div class="mt-4 flex items-center justify-between">
<div>
<h2 class="m-0 text-lg font-extrabold text-contrast">Hide Nametag</h2>
<h2 class="m-0 text-lg font-extrabold text-contrast">Hide nametag</h2>
<p class="m-0 mt-1">
Allows you to disable the nametag that appears above the main preview on the skin management
Disables the nametag above your player on the skins page.
page.
</p>
</div>
@@ -69,7 +69,7 @@ watch(
<div v-if="os !== 'MacOS'" class="mt-4 flex items-center justify-between gap-4">
<div>
<h2 class="m-0 text-lg font-extrabold text-contrast">Native Decorations</h2>
<h2 class="m-0 text-lg font-extrabold text-contrast">Native decorations</h2>
<p class="m-0 mt-1">Use system window frame (app restart required).</p>
</div>
<Toggle id="native-decorations" v-model="settings.native_decorations" />

View File

@@ -1,8 +1,9 @@
<template>
<NewModal ref="modal" @on-hide="resetState">
<UploadSkinModal ref="uploadModal" />
<ModalWrapper ref="modal" @on-hide="resetState">
<template #title>
<span class="text-lg font-extrabold text-contrast">
{{ mode === 'edit' ? 'Edit skin' : 'New skin' }}
{{ mode === 'edit' ? 'Editing skin' : 'Adding a skin' }}
</span>
</template>
@@ -78,16 +79,16 @@
<div class="flex gap-2 mt-12">
<ButtonStyled color="brand" :disabled="disableSave || isSaving">
<Button v-tooltip="saveTooltip" :disabled="disableSave || isSaving" @click="save">
<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>
</button>
</ButtonStyled>
<Button :disabled="isSaving" @click="hide"><XIcon />Cancel</Button>
</div>
</NewModal>
</ModalWrapper>
<SelectCapeModal
ref="selectCapeModal"
@@ -127,6 +128,8 @@ import {
ChevronRightIcon,
SpinnerIcon,
} from '@modrinth/assets'
import ModalWrapper from "@/components/ui/modal/ModalWrapper.vue";
import UploadSkinModal from "@/components/ui/skin/UploadSkinModal.vue";
const modal = useTemplateRef('modal')
const selectCapeModal = useTemplateRef('selectCapeModal')
@@ -337,6 +340,16 @@ function openUploadSkinModal(e: MouseEvent) {
emit('open-upload-modal', e)
}
function restoreModal() {
if (shouldRestoreModal.value) {
setTimeout(() => {
const fakeEvent = new MouseEvent('click')
modal.value?.show(fakeEvent)
shouldRestoreModal.value = false
}, 500);
}
}
async function save() {
isSaving.value = true
@@ -394,5 +407,6 @@ defineExpose({
restoreWithNewTexture,
hide,
shouldRestoreModal,
restoreModal
})
</script>

View File

@@ -7,9 +7,9 @@ import {
CapeButton,
CapeLikeTextButton,
SkinPreviewRenderer,
NewModal,
} from '@modrinth/ui'
import { CheckIcon, XIcon } from '@modrinth/assets'
import ModalWrapper from "@/components/ui/modal/ModalWrapper.vue";
const modal = useTemplateRef('modal')
@@ -74,7 +74,7 @@ defineExpose({
})
</script>
<template>
<NewModal ref="modal" @on-hide="onModalHide">
<ModalWrapper ref="modal" @on-hide="onModalHide">
<template #title>
<div class="flex flex-col">
<span class="text-lg font-extrabold text-heading">Change cape</span>
@@ -139,5 +139,5 @@ defineExpose({
</button>
</ButtonStyled>
</div>
</NewModal>
</ModalWrapper>
</template>

View File

@@ -1,5 +1,5 @@
<template>
<NewModal ref="modal" @on-hide="hide(true)">
<ModalWrapper ref="modal" @on-hide="hide(true)">
<template #title>
<span class="text-lg font-extrabold text-contrast"> Upload skin texture </span>
</template>
@@ -7,10 +7,8 @@
<div
class="border-2 border-dashed border-highlight-gray rounded-xl h-[173px] flex flex-col items-center justify-center p-8 cursor-pointer bg-button-bg hover:bg-button-hover transition-colors relative"
@click="triggerFileInput"
@drop.prevent="handleFileOperation"
@dragover.prevent
>
<p class="mx-auto mb-0 text-primary text-xl text-center flex items-center gap-2">
<p class="mx-auto mb-0 text-primary font-bold text-lg 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">
@@ -21,23 +19,27 @@
type="file"
accept="image/png"
class="hidden"
@change="handleFileOperation"
@change="handleInputFileChange"
/>
</div>
</div>
</NewModal>
</ModalWrapper>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { ref, onBeforeUnmount, watch } from 'vue'
import { UploadIcon } from '@modrinth/assets'
import { useNotifications } from '@/store/state'
import { NewModal } from '@modrinth/ui'
import { getCurrentWebview } from '@tauri-apps/api/webview'
import { readFile } from '@tauri-apps/plugin-fs'
import ModalWrapper from "@/components/ui/modal/ModalWrapper.vue";
const notifications = useNotifications()
const modal = ref()
const fileInput = ref<HTMLInputElement>()
const unlisten = ref<() => void>()
const modalVisible = ref(false)
const emit = defineEmits<{
(e: 'uploaded', file: File): void
@@ -46,10 +48,14 @@ const emit = defineEmits<{
function show(e?: MouseEvent) {
modal.value?.show(e)
modalVisible.value = true
setupDragDropListener()
}
function hide(emitCanceled = false) {
modal.value?.hide()
modalVisible.value = false
cleanupDragDropListener()
resetState()
if (emitCanceled) {
emit('canceled')
@@ -64,28 +70,77 @@ function triggerFileInput() {
fileInput.value?.click()
}
async function validateImageDimensions(file: File): Promise<boolean> {
async function validateImageDimensions(blob: Blob): Promise<boolean> {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => {
URL.revokeObjectURL(img.src)
resolve(img.width === 64 && (img.height === 64 || img.height == 32))
resolve(img.width === 64 && (img.height === 64 || img.height === 32))
}
img.onerror = () => {
URL.revokeObjectURL(img.src)
resolve(false)
}
img.src = URL.createObjectURL(file)
img.src = URL.createObjectURL(blob)
})
}
async function handleFileOperation(e: Event | DragEvent) {
const files = (e as DragEvent).dataTransfer?.files || (e.target as HTMLInputElement).files
async function handleInputFileChange(e: Event) {
const files = (e.target as HTMLInputElement).files
if (!files || files.length === 0) {
return
}
const file = files[0]
if (file.type !== 'image/png') {
await processFile(file)
}
async function setupDragDropListener() {
try {
if (modalVisible.value) {
await cleanupDragDropListener()
unlisten.value = await getCurrentWebview().onDragDropEvent(async (event) => {
if (event.payload.type !== 'drop') {
return
}
if (!event.payload.paths || event.payload.paths.length === 0) {
return
}
const filePath = event.payload.paths[0]
try {
const data = await readFile(filePath);
const fileName = filePath.split('/').pop() || filePath.split('\\').pop() || 'skin.png'
const fileBlob = new Blob([data], { type: 'image/png' })
const file = new File([fileBlob], fileName, { type: 'image/png' })
await processFile(file)
} catch (error) {
console.error(error);
notifications.addNotification({
title: 'Error processing file',
text: 'Failed to read the dropped file.',
type: 'error',
})
}
})
}
} catch (error) {
console.error('Failed to set up drag and drop listener:', error)
}
}
async function cleanupDragDropListener() {
if (unlisten.value) {
unlisten.value()
unlisten.value = undefined
}
}
async function processFile(file: File) {
if (!file.name.toLowerCase().endsWith('.png') && file.type !== 'image/png') {
notifications.addNotification({
title: 'Invalid file type.',
text: 'Only PNG files are accepted.',
@@ -93,6 +148,7 @@ async function handleFileOperation(e: Event | DragEvent) {
})
return
}
const isValidDimensions = await validateImageDimensions(file)
if (!isValidDimensions) {
notifications.addNotification({
@@ -102,9 +158,22 @@ async function handleFileOperation(e: Event | DragEvent) {
})
return
}
emit('uploaded', file)
hide()
}
watch(modalVisible, (isVisible) => {
if (isVisible) {
setupDragDropListener()
} else {
cleanupDragDropListener()
}
})
onBeforeUnmount(() => {
cleanupDragDropListener()
})
defineExpose({ show, hide })
</script>

View File

@@ -33,12 +33,12 @@ export class SkinPreviewStorage {
async store(key: string, result: RenderResult): Promise<void> {
if (!this.db) await this.init()
const transaction = this.db!.transaction(['previews'], 'readwrite')
const store = transaction.objectStore('previews')
const forwardsBlob = await fetch(result.forwards).then((r) => r.blob())
const backwardsBlob = await fetch(result.backwards).then((r) => r.blob())
const transaction = this.db!.transaction(['previews'], 'readwrite')
const store = transaction.objectStore('previews')
const storedPreview: StoredPreview = {
forwards: forwardsBlob,
backwards: backwardsBlob,

View File

@@ -252,13 +252,8 @@ function onSkinFileUploaded(file: File) {
}
function onUploadCanceled() {
if (editSkinModal.value && editSkinModal.value.shouldRestoreModal) {
setTimeout(() => {
const fakeEvent = new MouseEvent('click')
editSkinModal.value!.show(fakeEvent)
editSkinModal.value!.shouldRestoreModal = false
}, 0)
}
console.log("fuck fuck fuck")
editSkinModal.value?.restoreModal()
}
watch(
@@ -363,7 +358,7 @@ 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">
<SkinLikeTextButton class="skin-card" @click="openUploadSkinModal">
<template #icon>
<PlusIcon class="size-8" />
</template>
@@ -382,10 +377,9 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
<template #overlay-buttons>
<Button
color="green"
class="pointer-events-auto"
aria-label="Edit skin"
@click.stop="(e) => editSkinModal?.show(e, skin)"
>
class="pointer-events-auto">
<EditIcon /> Edit
</Button>
<Button
@@ -451,11 +445,11 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
Modrinth app.
</p>
<ButtonStyled v-show="accountsCard" color="brand" :disabled="accountsCard.loginDisabled">
<Button :disabled="accountsCard.loginDisabled" @click="login">
<button :disabled="accountsCard.loginDisabled" @click="login">
<LogInIcon v-if="!accountsCard.loginDisabled" />
<SpinnerIcon v-else class="animate-spin" />
Sign In
</Button>
</button>
</ButtonStyled>
</div>
</div>