diff --git a/src/components/dialogs/AvatarDialog/AvatarDialog.vue b/src/components/dialogs/AvatarDialog/AvatarDialog.vue index a9b4d0f4..afdddca6 100644 --- a/src/components/dialogs/AvatarDialog/AvatarDialog.vue +++ b/src/components/dialogs/AvatarDialog/AvatarDialog.vue @@ -371,10 +371,18 @@ :disabled="!!avatarDialog.galleryLoading" size="small" :icon="Upload" + :loading="!!avatarDialog.galleryLoading" style="margin-left: 5px" @click="displayAvatarGalleryUpload" >{{ t('dialog.screenshot_metadata.upload') }} + = 100000000) { - // 100MB - ElMessage({ - message: t('message.file.too_large'), - type: 'error' - }); - clearFile(); - return; - } - if (!files[0].type.match(/image.*/)) { - ElMessage({ - message: t('message.file.not_image'), - type: 'error' - }); - clearFile(); + const { file, clearInput } = handleImageUploadInput(e, { + inputSelector: '#AvatarGalleryUploadButton', + tooLargeMessage: () => t('message.file.too_large'), + invalidTypeMessage: () => t('message.file.not_image') + }); + if (!file) { return; } const r = new FileReader(); - r.onload = function () { - avatarDialog.value.galleryLoading = true; - const base64Body = btoa(r.result.toString()); - avatarRequest - .uploadAvatarGalleryImage(base64Body, avatarDialog.value.id) - .then(async (args) => { - ElMessage({ - message: t('message.avatar_gallery.uploaded'), - type: 'success' - }); - console.log(args); - avatarDialog.value.galleryImages = await getAvatarGallery(avatarDialog.value.id); - return args; - }) - .finally(() => { - avatarDialog.value.galleryLoading = false; - }); + const resetLoading = () => { + avatarDialog.value.galleryLoading = false; + clearInput(); }; - r.readAsBinaryString(files[0]); - clearFile(); + r.onerror = resetLoading; + r.onabort = resetLoading; + r.onload = function () { + try { + avatarDialog.value.galleryLoading = true; + const base64Body = btoa(r.result.toString()); + avatarRequest + .uploadAvatarGalleryImage(base64Body, avatarDialog.value.id) + .then(async (args) => { + ElMessage({ + message: t('message.avatar_gallery.uploaded'), + type: 'success' + }); + console.log(args); + avatarDialog.value.galleryImages = await getAvatarGallery(avatarDialog.value.id); + return args; + }) + .catch((error) => { + console.error('Failed to upload image', error); + }) + .finally(resetLoading); + } catch (error) { + console.error('Failed to process image', error); + resetLoading(); + } + }; + try { + r.readAsBinaryString(file); + } catch (error) { + console.error('Failed to read file', error); + resetLoading(); + } } function reorderAvatarGalleryImage(imageUrl, direction) { diff --git a/src/components/dialogs/AvatarDialog/ChangeAvatarImageDialog.vue b/src/components/dialogs/AvatarDialog/ChangeAvatarImageDialog.vue index eaeb09d2..e87a4d3d 100644 --- a/src/components/dialogs/AvatarDialog/ChangeAvatarImageDialog.vue +++ b/src/components/dialogs/AvatarDialog/ChangeAvatarImageDialog.vue @@ -6,17 +6,30 @@ width="850px" append-to-body @close="closeDialog"> -
+
+ {{ t('dialog.change_content_image.description') }}
- + {{ t('dialog.change_content_image.upload') }} @@ -35,6 +48,7 @@ import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { avatarRequest } from '../../../api'; + import { handleImageUploadInput } from '../../../shared/utils/imageUpload'; import { useAvatarStore } from '../../../stores'; const { t } = useI18n(); @@ -68,39 +82,26 @@ } function onFileChangeAvatarImage(e) { - const clearFile = function () { - changeAvatarImageDialogLoading.value = false; - const fileInput = /** @type{HTMLInputElement} */ (document.querySelector('#AvatarImageUploadButton')); - if (fileInput) { - fileInput.value = ''; - } - }; - const files = e.target.files || e.dataTransfer.files; - if (!files.length || !avatarDialog.value.visible || avatarDialog.value.loading) { - clearFile(); + const { file, clearInput } = handleImageUploadInput(e, { + inputSelector: '#AvatarImageUploadButton', + tooLargeMessage: () => t('message.file.too_large'), + invalidTypeMessage: () => t('message.file.not_image') + }); + if (!file) { return; } - - // validate file - if (files[0].size >= 100000000) { - // 100MB - ElMessage({ - message: t('message.file.too_large'), - type: 'error' - }); - clearFile(); - return; - } - if (!files[0].type.match(/image.*/)) { - ElMessage({ - message: t('message.file.not_image'), - type: 'error' - }); - clearFile(); + if (!avatarDialog.value.visible || avatarDialog.value.loading) { + clearInput(); return; } const r = new FileReader(); + const finalize = () => { + changeAvatarImageDialogLoading.value = false; + clearInput(); + }; + r.onerror = finalize; + r.onabort = finalize; r.onload = async function () { try { const base64File = await resizeImageToFitLimits(btoa(r.result.toString())); @@ -109,12 +110,17 @@ } catch (error) { console.error('Avatar image upload process failed:', error); } finally { - clearFile(); + finalize(); } }; changeAvatarImageDialogLoading.value = true; - r.readAsBinaryString(files[0]); + try { + r.readAsBinaryString(file); + } catch (error) { + console.error('Failed to read file', error); + finalize(); + } } async function initiateUpload(base64File) { diff --git a/src/components/dialogs/WorldDialog/ChangeWorldImageDialog.vue b/src/components/dialogs/WorldDialog/ChangeWorldImageDialog.vue index 35a23d67..25ce239c 100644 --- a/src/components/dialogs/WorldDialog/ChangeWorldImageDialog.vue +++ b/src/components/dialogs/WorldDialog/ChangeWorldImageDialog.vue @@ -6,17 +6,30 @@ width="850px" append-to-body @close="closeDialog"> -
+
+ {{ t('dialog.change_content_image.description') }}
- + {{ t('dialog.change_content_image.upload') }} @@ -35,6 +48,7 @@ import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { worldRequest } from '../../../api'; + import { handleImageUploadInput } from '../../../shared/utils/imageUpload'; import { useWorldStore } from '../../../stores'; const { t } = useI18n(); @@ -42,7 +56,7 @@ const { worldDialog } = storeToRefs(useWorldStore()); const { applyWorld } = useWorldStore(); - const props = defineProps({ + defineProps({ changeWorldImageDialogVisible: { type: Boolean, required: true @@ -67,39 +81,26 @@ } function onFileChangeWorldImage(e) { - const clearFile = function () { - changeWorldImageDialogLoading.value = false; - const fileInput = /** @type{HTMLInputElement} */ (document.querySelector('#WorldImageUploadButton')); - if (fileInput) { - fileInput.value = ''; - } - }; - const files = e.target.files || e.dataTransfer.files; - if (!files.length || !worldDialog.value.visible || worldDialog.value.loading) { - clearFile(); + const { file, clearInput } = handleImageUploadInput(e, { + inputSelector: '#WorldImageUploadButton', + tooLargeMessage: () => t('message.file.too_large'), + invalidTypeMessage: () => t('message.file.not_image') + }); + if (!file) { return; } - - // validate file - if (files[0].size >= 100000000) { - // 100MB - ElMessage({ - message: t('message.file.too_large'), - type: 'error' - }); - clearFile(); - return; - } - if (!files[0].type.match(/image.*/)) { - ElMessage({ - message: t('message.file.not_image'), - type: 'error' - }); - clearFile(); + if (!worldDialog.value.visible || worldDialog.value.loading) { + clearInput(); return; } const r = new FileReader(); + const finalize = () => { + changeWorldImageDialogLoading.value = false; + clearInput(); + }; + r.onerror = finalize; + r.onabort = finalize; r.onload = async function () { try { const base64File = await resizeImageToFitLimits(btoa(r.result.toString())); @@ -108,12 +109,17 @@ } catch (error) { console.error('World image upload process failed:', error); } finally { - clearFile(); + finalize(); } }; changeWorldImageDialogLoading.value = true; - r.readAsBinaryString(files[0]); + try { + r.readAsBinaryString(file); + } catch (error) { + console.error('Failed to read file', error); + finalize(); + } } async function initiateUpload(base64File) { diff --git a/src/shared/utils/imageUpload.js b/src/shared/utils/imageUpload.js new file mode 100644 index 00000000..453faacb --- /dev/null +++ b/src/shared/utils/imageUpload.js @@ -0,0 +1,70 @@ +import { ElMessage } from 'element-plus'; + +function resolveMessage(message) { + if (typeof message === 'function') { + return message(); + } + return message; +} + +function getInputElement(selector) { + if (!selector) { + return null; + } + if (typeof selector === 'function') { + return selector(); + } + if (typeof selector === 'string') { + return document.querySelector(selector); + } + return selector; +} + +export function handleImageUploadInput(event, options = {}) { + const { + inputSelector, + maxSize = 100000000, + acceptPattern = /image.*/, + tooLargeMessage, + invalidTypeMessage, + onClear + } = options; + + const clearInput = () => { + onClear?.(); + const input = getInputElement(inputSelector); + if (input) { + input.value = ''; + } + }; + + const files = event?.target?.files || event?.dataTransfer?.files; + if (!files || files.length === 0) { + clearInput(); + return { file: null, clearInput }; + } + + const file = files[0]; + if (file.size >= maxSize) { + if (tooLargeMessage) { + ElMessage({ message: resolveMessage(tooLargeMessage), type: 'error' }); + } + clearInput(); + return { file: null, clearInput }; + } + + let acceptRegex = null; + if (acceptPattern) { + acceptRegex = acceptPattern instanceof RegExp ? acceptPattern : new RegExp(acceptPattern); + } + + if (acceptRegex && !acceptRegex.test(file.type)) { + if (invalidTypeMessage) { + ElMessage({ message: resolveMessage(invalidTypeMessage), type: 'error' }); + } + clearInput(); + return { file: null, clearInput }; + } + + return { file, clearInput }; +} diff --git a/src/stores/gallery.js b/src/stores/gallery.js index 439ff2db..3dfdf348 100644 --- a/src/stores/gallery.js +++ b/src/stores/gallery.js @@ -1,7 +1,6 @@ import Noty from 'noty'; import { defineStore } from 'pinia'; import { computed, reactive, watch } from 'vue'; -import { ElMessage } from 'element-plus'; import * as workerTimers from 'worker-timers'; import { inventoryRequest, @@ -16,6 +15,7 @@ import { getPrintFileName, getPrintLocalDate } from '../shared/utils'; +import { handleImageUploadInput } from '../shared/utils/imageUpload'; import { useAdvancedSettingsStore } from './settings/advanced'; import { useI18n } from 'vue-i18n'; @@ -266,32 +266,20 @@ export const useGalleryStore = defineStore('Gallery', () => { } function inviteImageUpload(e) { - const files = e.target.files || e.dataTransfer.files; - if (!files.length) { - return; - } - if (files[0].size >= 100000000) { - // 100MB - ElMessage({ - message: t('message.file.too_large'), - type: 'error' - }); - clearInviteImageUpload(); - return; - } - if (!files[0].type.match(/image.*/)) { - ElMessage({ - message: t('message.file.not_image'), - type: 'error' - }); - clearInviteImageUpload(); + const { file } = handleImageUploadInput(e, { + inputSelector: null, + tooLargeMessage: () => t('message.file.too_large'), + invalidTypeMessage: () => t('message.file.not_image'), + onClear: clearInviteImageUpload + }); + if (!file) { return; } const r = new FileReader(); r.onload = function () { state.uploadImage = btoa(r.result); }; - r.readAsBinaryString(files[0]); + r.readAsBinaryString(file); } function clearInviteImageUpload() { diff --git a/src/views/Tools/dialogs/GalleryDialog.vue b/src/views/Tools/dialogs/GalleryDialog.vue index f2832c62..178d9ee1 100644 --- a/src/views/Tools/dialogs/GalleryDialog.vue +++ b/src/views/Tools/dialogs/GalleryDialog.vue @@ -6,6 +6,13 @@ width="97vw" append-to-body @close="closeGalleryDialog"> +