Compress answers and show upload progress
This commit is contained in:
+3
-1
@@ -114,7 +114,9 @@
|
||||
"required_answer": "Erforderliche Antwort",
|
||||
"finalize": "Antwort finalisieren",
|
||||
"already_submitted": "Deine Antwort wurde bereits übermittelt!",
|
||||
"finalize_explain": "Dies gibt den Admins die Möglichkeit deine Antwort zu bewerten und dir Punkte zu geben. Du kannst deine Antwort anschließend nicht mehr ändern!"
|
||||
"finalize_explain": "Dies gibt den Admins die Möglichkeit deine Antwort zu bewerten und dir Punkte zu geben. Du kannst deine Antwort anschließend nicht mehr ändern!",
|
||||
"upload_title": "Antwort wird hochgeladen...",
|
||||
"upload_compress": "Bild wird komprimiert..."
|
||||
},
|
||||
"hunt": {
|
||||
"total_points": "Punktestand: {score}",
|
||||
|
||||
+3
-1
@@ -114,7 +114,9 @@
|
||||
"required_answer": "Required Answer",
|
||||
"finalize": "Finalize answer",
|
||||
"already_submitted": "Your final answer has already been submitted!",
|
||||
"finalize_explain": "This allows the admins to review your answer and give you points. You cannot change your answer afterwards!"
|
||||
"finalize_explain": "This allows the admins to review your answer and give you points. You cannot change your answer afterwards!",
|
||||
"upload_title": "Uploading answer...",
|
||||
"upload_compress": "Compressing image..."
|
||||
},
|
||||
"hunt": {
|
||||
"total_points": "Total points: {score}",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { createImageURL } from '#shared/utils/image';
|
||||
import { compressImage, createImageURL } from '#shared/utils/image';
|
||||
import ClickableImage from '~/components/ClickableImage.vue';
|
||||
import MultilineString from '~/components/MultilineString.vue';
|
||||
import { useI18nKey } from '~/composables/useI18nKey';
|
||||
@@ -55,11 +55,24 @@ const textType = {
|
||||
|
||||
const newImage = ref<File>();
|
||||
|
||||
const newImageURL = computed(() =>
|
||||
newImage.value ? createImageURL(newImage.value) : undefined
|
||||
);
|
||||
|
||||
const uploading = ref(false);
|
||||
const uploadingImage = ref(false);
|
||||
const compressing = ref(false);
|
||||
const uploadProgress = ref(1);
|
||||
const submitError = ref('');
|
||||
useEventListener(window, 'beforeunload', (ev) => {
|
||||
if (uploading.value) {
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
async function submit() {
|
||||
uploading.value = true;
|
||||
submitError.value = '';
|
||||
uploadProgress.value = 0;
|
||||
|
||||
const body = new FormData();
|
||||
body.set('lang', locale.value);
|
||||
@@ -74,27 +87,38 @@ async function submit() {
|
||||
body.set('final', 'true');
|
||||
}
|
||||
if (newImage.value) {
|
||||
body.set('image', newImage.value);
|
||||
uploadingImage.value = true;
|
||||
compressing.value = true;
|
||||
const compressedImage = await compressImage(newImage.value);
|
||||
compressing.value = false;
|
||||
body.set('image', compressedImage);
|
||||
}
|
||||
const xhr = new XMLHttpRequest();
|
||||
|
||||
try {
|
||||
await $fetch(`/api/quest/${questSlug?.id}/submit`, {
|
||||
method: 'POST',
|
||||
body
|
||||
await new Promise((resolve) => {
|
||||
xhr.upload.addEventListener('progress', (event) => {
|
||||
if (event.lengthComputable) {
|
||||
uploadProgress.value = event.loaded / event.total;
|
||||
}
|
||||
});
|
||||
xhr.addEventListener('loadend', () => {
|
||||
resolve(xhr.readyState === 4 && xhr.status === 200);
|
||||
uploadProgress.value = 1;
|
||||
});
|
||||
xhr.open('POST', `/api/quest/${questSlug?.id}/submit`, true);
|
||||
xhr.withCredentials = true;
|
||||
xhr.send(body);
|
||||
});
|
||||
|
||||
await refresh();
|
||||
answer.value = data.value?.answer?.text;
|
||||
newImage.value = undefined;
|
||||
} catch (e) {
|
||||
if (e instanceof Error && 'statusCode' in e && e.statusCode === 418) {
|
||||
if (xhr.status === 418) {
|
||||
submitError.value = t('page.quest.already_finalized');
|
||||
await refresh();
|
||||
} else if (
|
||||
e instanceof Error &&
|
||||
'statusCode' in e &&
|
||||
e.statusCode === 409
|
||||
) {
|
||||
} else if (xhr.status === 409) {
|
||||
submitError.value = t('page.quest.conflict');
|
||||
} else {
|
||||
submitError.value = parseError(e);
|
||||
@@ -102,11 +126,31 @@ async function submit() {
|
||||
}
|
||||
|
||||
uploading.value = false;
|
||||
uploadingImage.value = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<VaModal
|
||||
:model-value="uploadingImage"
|
||||
no-outside-dismiss
|
||||
hide-default-actions
|
||||
no-dismiss
|
||||
>
|
||||
<h2 class="text-2xl font-bold">{{ t('page.quest.upload_title') }}</h2>
|
||||
<p v-if="compressing" class="text-lg">
|
||||
{{ t('page.quest.upload_compress') }}
|
||||
</p>
|
||||
<VaProgressCircle
|
||||
v-else
|
||||
class="mx-auto"
|
||||
:model-value="uploadProgress * 100"
|
||||
size="50dvw"
|
||||
>
|
||||
{{ (uploadProgress * 100).toFixed(0) + '%' }}
|
||||
</VaProgressCircle>
|
||||
</VaModal>
|
||||
<VaButtonGroup>
|
||||
<VaButton
|
||||
icon="arrow_back"
|
||||
@@ -230,8 +274,8 @@ async function submit() {
|
||||
</VaInput>
|
||||
|
||||
<VaImage
|
||||
v-if="newImage"
|
||||
:src="createImageURL(newImage)"
|
||||
v-if="newImageURL"
|
||||
:src="newImageURL"
|
||||
class="max-h-60 w-full"
|
||||
fit="contain"
|
||||
lazy
|
||||
@@ -279,7 +323,7 @@ async function submit() {
|
||||
newImage ||
|
||||
data.answer?.text !== answer ||
|
||||
data.answer?.final !== final
|
||||
)
|
||||
) || uploading
|
||||
"
|
||||
:loading="uploading"
|
||||
@click="submit"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@vuestic/tailwind": "^0.1.5",
|
||||
"@vueuse/nuxt": "13.6.0",
|
||||
"argon2": "^0.43.1",
|
||||
"compressorjs": "^1.2.1",
|
||||
"eslint": "^9.0.0",
|
||||
"h3-zod": "^0.5.3",
|
||||
"minio": "^8.0.5",
|
||||
@@ -777,6 +778,8 @@
|
||||
|
||||
"block-stream2": ["block-stream2@2.1.0", "", { "dependencies": { "readable-stream": "^3.4.0" } }, "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg=="],
|
||||
|
||||
"blueimp-canvas-to-blob": ["blueimp-canvas-to-blob@3.29.0", "", {}, "sha512-0pcSSGxC0QxT+yVkivxIqW0Y4VlO2XSDPofBAqoJ1qJxgH9eiUDLv50Rixij2cDuEfx4M6DpD9UGZpRhT5Q8qg=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
@@ -875,6 +878,8 @@
|
||||
|
||||
"compress-commons": ["compress-commons@6.0.2", "", { "dependencies": { "crc-32": "^1.2.0", "crc32-stream": "^6.0.0", "is-stream": "^2.0.1", "normalize-path": "^3.0.0", "readable-stream": "^4.0.0" } }, "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg=="],
|
||||
|
||||
"compressorjs": ["compressorjs@1.2.1", "", { "dependencies": { "blueimp-canvas-to-blob": "^3.29.0", "is-blob": "^2.1.0" } }, "sha512-+geIjeRnPhQ+LLvvA7wxBQE5ddeLU7pJ3FsKFWirDw6veY3s9iLxAQEw7lXGHnhCJvBujEQWuNnGzZcvCvdkLQ=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
||||
@@ -1295,6 +1300,8 @@
|
||||
|
||||
"is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
|
||||
|
||||
"is-blob": ["is-blob@2.1.0", "", {}, "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw=="],
|
||||
|
||||
"is-builtin-module": ["is-builtin-module@5.0.0", "", { "dependencies": { "builtin-modules": "^5.0.0" } }, "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA=="],
|
||||
|
||||
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"@vuestic/tailwind": "^0.1.5",
|
||||
"@vueuse/nuxt": "13.6.0",
|
||||
"argon2": "^0.43.1",
|
||||
"compressorjs": "^1.2.1",
|
||||
"eslint": "^9.0.0",
|
||||
"h3-zod": "^0.5.3",
|
||||
"minio": "^8.0.5",
|
||||
|
||||
@@ -1 +1,22 @@
|
||||
import Compressor from 'compressorjs';
|
||||
|
||||
export const createImageURL = (file: File) => URL.createObjectURL(file);
|
||||
|
||||
export async function compressImage(file: File) {
|
||||
return new Promise<File | Blob>((resolve, reject) => {
|
||||
if (!file) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
new Compressor(file, {
|
||||
quality: 0.8,
|
||||
error(error) {
|
||||
console.error(error);
|
||||
reject(error);
|
||||
},
|
||||
success(result) {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
}).catch(() => file);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user