Compress answers and show upload progress
This commit is contained in:
+3
-1
@@ -114,7 +114,9 @@
|
|||||||
"required_answer": "Erforderliche Antwort",
|
"required_answer": "Erforderliche Antwort",
|
||||||
"finalize": "Antwort finalisieren",
|
"finalize": "Antwort finalisieren",
|
||||||
"already_submitted": "Deine Antwort wurde bereits übermittelt!",
|
"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": {
|
"hunt": {
|
||||||
"total_points": "Punktestand: {score}",
|
"total_points": "Punktestand: {score}",
|
||||||
|
|||||||
+3
-1
@@ -114,7 +114,9 @@
|
|||||||
"required_answer": "Required Answer",
|
"required_answer": "Required Answer",
|
||||||
"finalize": "Finalize answer",
|
"finalize": "Finalize answer",
|
||||||
"already_submitted": "Your final answer has already been submitted!",
|
"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": {
|
"hunt": {
|
||||||
"total_points": "Total points: {score}",
|
"total_points": "Total points: {score}",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { createImageURL } from '#shared/utils/image';
|
import { compressImage, createImageURL } from '#shared/utils/image';
|
||||||
import ClickableImage from '~/components/ClickableImage.vue';
|
import ClickableImage from '~/components/ClickableImage.vue';
|
||||||
import MultilineString from '~/components/MultilineString.vue';
|
import MultilineString from '~/components/MultilineString.vue';
|
||||||
import { useI18nKey } from '~/composables/useI18nKey';
|
import { useI18nKey } from '~/composables/useI18nKey';
|
||||||
@@ -55,11 +55,24 @@ const textType = {
|
|||||||
|
|
||||||
const newImage = ref<File>();
|
const newImage = ref<File>();
|
||||||
|
|
||||||
|
const newImageURL = computed(() =>
|
||||||
|
newImage.value ? createImageURL(newImage.value) : undefined
|
||||||
|
);
|
||||||
|
|
||||||
const uploading = ref(false);
|
const uploading = ref(false);
|
||||||
|
const uploadingImage = ref(false);
|
||||||
|
const compressing = ref(false);
|
||||||
|
const uploadProgress = ref(1);
|
||||||
const submitError = ref('');
|
const submitError = ref('');
|
||||||
|
useEventListener(window, 'beforeunload', (ev) => {
|
||||||
|
if (uploading.value) {
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
async function submit() {
|
async function submit() {
|
||||||
uploading.value = true;
|
uploading.value = true;
|
||||||
submitError.value = '';
|
submitError.value = '';
|
||||||
|
uploadProgress.value = 0;
|
||||||
|
|
||||||
const body = new FormData();
|
const body = new FormData();
|
||||||
body.set('lang', locale.value);
|
body.set('lang', locale.value);
|
||||||
@@ -74,27 +87,38 @@ async function submit() {
|
|||||||
body.set('final', 'true');
|
body.set('final', 'true');
|
||||||
}
|
}
|
||||||
if (newImage.value) {
|
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 {
|
try {
|
||||||
await $fetch(`/api/quest/${questSlug?.id}/submit`, {
|
await new Promise((resolve) => {
|
||||||
method: 'POST',
|
xhr.upload.addEventListener('progress', (event) => {
|
||||||
body
|
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();
|
await refresh();
|
||||||
answer.value = data.value?.answer?.text;
|
answer.value = data.value?.answer?.text;
|
||||||
newImage.value = undefined;
|
newImage.value = undefined;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error && 'statusCode' in e && e.statusCode === 418) {
|
if (xhr.status === 418) {
|
||||||
submitError.value = t('page.quest.already_finalized');
|
submitError.value = t('page.quest.already_finalized');
|
||||||
await refresh();
|
await refresh();
|
||||||
} else if (
|
} else if (xhr.status === 409) {
|
||||||
e instanceof Error &&
|
|
||||||
'statusCode' in e &&
|
|
||||||
e.statusCode === 409
|
|
||||||
) {
|
|
||||||
submitError.value = t('page.quest.conflict');
|
submitError.value = t('page.quest.conflict');
|
||||||
} else {
|
} else {
|
||||||
submitError.value = parseError(e);
|
submitError.value = parseError(e);
|
||||||
@@ -102,11 +126,31 @@ async function submit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uploading.value = false;
|
uploading.value = false;
|
||||||
|
uploadingImage.value = false;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<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>
|
<VaButtonGroup>
|
||||||
<VaButton
|
<VaButton
|
||||||
icon="arrow_back"
|
icon="arrow_back"
|
||||||
@@ -230,8 +274,8 @@ async function submit() {
|
|||||||
</VaInput>
|
</VaInput>
|
||||||
|
|
||||||
<VaImage
|
<VaImage
|
||||||
v-if="newImage"
|
v-if="newImageURL"
|
||||||
:src="createImageURL(newImage)"
|
:src="newImageURL"
|
||||||
class="max-h-60 w-full"
|
class="max-h-60 w-full"
|
||||||
fit="contain"
|
fit="contain"
|
||||||
lazy
|
lazy
|
||||||
@@ -279,7 +323,7 @@ async function submit() {
|
|||||||
newImage ||
|
newImage ||
|
||||||
data.answer?.text !== answer ||
|
data.answer?.text !== answer ||
|
||||||
data.answer?.final !== final
|
data.answer?.final !== final
|
||||||
)
|
) || uploading
|
||||||
"
|
"
|
||||||
:loading="uploading"
|
:loading="uploading"
|
||||||
@click="submit"
|
@click="submit"
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"@vuestic/tailwind": "^0.1.5",
|
"@vuestic/tailwind": "^0.1.5",
|
||||||
"@vueuse/nuxt": "13.6.0",
|
"@vueuse/nuxt": "13.6.0",
|
||||||
"argon2": "^0.43.1",
|
"argon2": "^0.43.1",
|
||||||
|
"compressorjs": "^1.2.1",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"h3-zod": "^0.5.3",
|
"h3-zod": "^0.5.3",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
@@ -777,6 +778,8 @@
|
|||||||
|
|
||||||
"block-stream2": ["block-stream2@2.1.0", "", { "dependencies": { "readable-stream": "^3.4.0" } }, "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
|
"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-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-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=="],
|
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"@vuestic/tailwind": "^0.1.5",
|
"@vuestic/tailwind": "^0.1.5",
|
||||||
"@vueuse/nuxt": "13.6.0",
|
"@vueuse/nuxt": "13.6.0",
|
||||||
"argon2": "^0.43.1",
|
"argon2": "^0.43.1",
|
||||||
|
"compressorjs": "^1.2.1",
|
||||||
"eslint": "^9.0.0",
|
"eslint": "^9.0.0",
|
||||||
"h3-zod": "^0.5.3",
|
"h3-zod": "^0.5.3",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
|
|||||||
@@ -1 +1,22 @@
|
|||||||
|
import Compressor from 'compressorjs';
|
||||||
|
|
||||||
export const createImageURL = (file: File) => URL.createObjectURL(file);
|
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