Quick save

This commit is contained in:
Mawoka
2023-06-09 10:07:47 +02:00
parent 44432b20c7
commit 28c072227c
13 changed files with 537 additions and 35 deletions
+76 -6
View File
@@ -2,8 +2,9 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
from datetime import datetime, timedelta
from tempfile import SpooledTemporaryFile
from fastapi import APIRouter, HTTPException, UploadFile, File, Depends
from fastapi import APIRouter, HTTPException, UploadFile, File, Depends, Request, Response
from fastapi.responses import StreamingResponse, RedirectResponse
from pydantic import BaseModel
@@ -19,8 +20,22 @@ settings = settings()
router = APIRouter()
def headers_from_storage_item(item: StorageItem) -> dict[str, str]:
base_headers = {"Content-Type": item.mime_type}
if item.hash is not None:
base_headers["X-Hash"] = item.hash.hex()
if item.thumbhash is not None:
base_headers["X-Thumbhash"] = item.thumbhash
if item.alt_text is not None:
base_headers["X-Alt-Text"] = item.alt_text
if item.size != 0:
base_headers["Content-Size"] = str(item.size)
return base_headers
@router.get("/download/{file_name}")
async def download_file(file_name: str):
item = None
checked_image_string = check_image_string(file_name)
if not checked_image_string[0]:
raise HTTPException(status_code=400, detail="Invalid file name")
@@ -32,7 +47,7 @@ async def download_file(file_name: str):
if file_name is None:
file_name = item.id.hex
if storage.backend == "s3":
return RedirectResponse(url=await storage.get_url(file_name, 300))
return RedirectResponse(url=await storage.get_url(file_name, 300), headers=headers_from_storage_item(item))
try:
download = await storage.download(file_name)
except DownloadingFailedError:
@@ -43,13 +58,43 @@ async def download_file(file_name: str):
def iter_file():
yield from download
media_type = "image/*"
if item is not None:
media_type = item.mime_type
headers = {"Cache-Control": "public, immutable, max-age=31536000"}
if item is not None:
headers = {**headers, **headers_from_storage_item(item)}
return StreamingResponse(
iter_file(),
media_type="image/*",
headers={"Cache-Control": "public, immutable, max-age=31536000"},
media_type=media_type,
headers=headers,
)
@router.head("/download/{file_name}")
async def download_file_head(file_name: str) -> Response:
checked_image_string = check_image_string(file_name)
if not checked_image_string[0]:
raise HTTPException(status_code=404, detail="Invalid file name")
if checked_image_string[1] is not None:
item = await StorageItem.objects.get_or_none(id=checked_image_string[1])
if item is None:
raise HTTPException(status_code=404, detail="File not found")
# return PublicStorageItem.from_db_model(item)
storage_file_name = item.storage_path
if storage_file_name is None:
storage_file_name = item.id.hex
resp = Response(status_code=200, headers=headers_from_storage_item(item))
else:
resp = Response(status_code=200, headers={"Content-Type": "image/*"})
storage_file_name = file_name
if storage.backend == "s3":
resp.status_code = 307
resp.headers.append("Location", await storage.get_url(storage_file_name, 300))
return resp
@router.post("/")
async def upload_file(file: UploadFile = File(), user: User = Depends(get_current_user)) -> PublicStorageItem:
if user.storage_used > settings.free_storage_limit:
@@ -67,8 +112,33 @@ async def upload_file(file: UploadFile = File(), user: User = Depends(get_curren
deleted_at=None,
alt_text=None,
)
file_data = await file.read()
await storage.upload(file_name=file_id.hex, file_data=file_data)
await storage.upload(file_name=file_id.hex, file_data=file.file)
await file_obj.save()
await arq.enqueue_job("calculate_hash", file_id.hex)
return PublicStorageItem.from_db_model(file_obj)
@router.post("/raw")
async def upload_raw_file(request: Request, user: User = Depends(get_current_user)) -> PublicStorageItem:
if user.storage_used > settings.free_storage_limit:
raise HTTPException(status_code=409, detail="Storage limit reached")
file_id = uuid4()
data_file = SpooledTemporaryFile(max_size=1000)
async for chunk in request.stream():
data_file.write(chunk)
data_file.seek(0)
file_obj = StorageItem(
id=file_id,
uploaded_at=datetime.now(),
mime_type=request.headers.get("Content-Type"),
hash=None,
user=user,
size=0,
deleted_at=None,
alt_text=None,
)
# https://github.com/VirusTotal/vt-py/issues/119#issuecomment-1261246867
await storage.upload(file_name=file_id.hex, file_data=data_file._file)
await file_obj.save()
await arq.enqueue_job("calculate_hash", file_id.hex)
return PublicStorageItem.from_db_model(file_obj)
+2 -2
View File
@@ -50,8 +50,8 @@ class Storage:
"""
yield self.instance.download(file_name)
async def upload(self, file_name: str, file_data: BinaryIO) -> None:
return await self.instance.upload(file=file_data, file_name=file_name)
async def upload(self, file_name: str, file_data: BinaryIO, mime_type: str | None = None) -> None:
return await self.instance.upload(file=file_data, file_name=file_name, mime_type=mime_type)
async def delete(self, file_names: [str]) -> None:
return await self.instance.delete(file_names=file_names)
+1 -1
View File
@@ -27,7 +27,7 @@ class LocalStorage:
except FileNotFoundError:
yield None
async def upload(self, file_name: str, data: BinaryIO) -> None:
async def upload(self, file_name: str, data: BinaryIO, mime_type: str | None = None) -> None:
async with aiofiles.open(file=os.path.join(self.base_path, file_name), mode="wb") as f:
await aioshutil_copyfileobj(data, f)
+1 -3
View File
@@ -112,9 +112,7 @@ class S3Storage:
return headers, request_url
async def upload(
self, file: BinaryIO, file_name: str, content_type: str | None = "application/octet-stream"
) -> None:
async def upload(self, file: BinaryIO, file_name: str, mime_type: str | None = "application/octet-stream") -> None:
headers, url = self._generate_aws_signature_v4(method="PUT", path=f"/{file_name}")
async with ClientSession() as session, session.put(url, headers=headers, data=file) as resp:
if resp.status == 200:
+3
View File
@@ -23,6 +23,8 @@
"@ckeditor/ckeditor5-theme-lark": "^37.1.0",
"@felte/reporter-tippy": "^1.1.5",
"@felte/validator-yup": "^1.0.11",
"@ffmpeg/core": "^0.11.0",
"@ffmpeg/ffmpeg": "^0.11.6",
"@fontsource/marck-script": "^4.5.11",
"@sentry/browser": "^7.48.0",
"@sentry/tracing": "^7.48.0",
@@ -96,6 +98,7 @@
"typescript": "~5.0.4",
"ua-parser-js": "^1.0.35",
"vite": "^4.2.2",
"vite-plugin-cross-origin-isolation": "^0.1.6",
"vite-plugin-iso-import": "^1.0.0",
"yup": "^1.1.1"
},
+89
View File
@@ -10,6 +10,8 @@ specifiers:
'@ckeditor/ckeditor5-theme-lark': ^37.1.0
'@felte/reporter-tippy': ^1.1.5
'@felte/validator-yup': ^1.0.11
'@ffmpeg/core': ^0.11.0
'@ffmpeg/ffmpeg': ^0.11.6
'@fontsource/marck-script': ^4.5.11
'@sentry/browser': ^7.48.0
'@sentry/tracing': ^7.48.0
@@ -84,6 +86,7 @@ specifiers:
typescript: ~5.0.4
ua-parser-js: ^1.0.35
vite: ^4.2.2
vite-plugin-cross-origin-isolation: ^0.1.6
vite-plugin-iso-import: ^1.0.0
yup: ^1.1.1
@@ -100,6 +103,8 @@ devDependencies:
'@ckeditor/ckeditor5-theme-lark': 37.1.0
'@felte/reporter-tippy': 1.1.5_tippy.js@6.3.7
'@felte/validator-yup': 1.0.11_yup@1.1.1
'@ffmpeg/core': 0.11.0
'@ffmpeg/ffmpeg': 0.11.6
'@fontsource/marck-script': 4.5.11
'@sentry/browser': 7.48.0
'@sentry/tracing': 7.48.0
@@ -173,6 +178,7 @@ devDependencies:
typescript: 5.0.4
ua-parser-js: 1.0.35
vite: 4.2.2_sass@1.62.0
vite-plugin-cross-origin-isolation: 0.1.6
vite-plugin-iso-import: 1.0.0_vite@4.2.2
yup: 1.1.1
@@ -939,6 +945,28 @@ packages:
yup: 1.1.1
dev: true
/@ffmpeg/core/0.11.0:
resolution:
{
integrity: sha512-9Tt/+2PMpkGPXUK8n6He9G8Y+qR6qmCPSCw9iEKZxHHOvJ9BE/r0Fccj+YgDZTlyu6rXxc9x6EqCaFBIt7qzjA==
}
dev: true
/@ffmpeg/ffmpeg/0.11.6:
resolution:
{
integrity: sha512-uN8J8KDjADEavPhNva6tYO9Fj0lWs9z82swF3YXnTxWMBoFLGq3LZ6FLlIldRKEzhOBKnkVfA8UnFJuvGvNxcA==
}
engines: { node: '>=12.16.1' }
dependencies:
is-url: 1.2.4
node-fetch: 2.6.11
regenerator-runtime: 0.13.11
resolve-url: 0.2.1
transitivePeerDependencies:
- encoding
dev: true
/@fontsource/marck-script/4.5.11:
resolution:
{
@@ -3538,6 +3566,13 @@ packages:
}
dev: true
/is-url/1.2.4:
resolution:
{
integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
}
dev: true
/isexe/2.0.0:
resolution:
{
@@ -4017,6 +4052,21 @@ packages:
}
dev: true
/node-fetch/2.6.11:
resolution:
{
integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==
}
engines: { node: 4.x || >=6.0.0 }
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
dependencies:
whatwg-url: 5.0.0
dev: true
/node-releases/2.0.10:
resolution:
{
@@ -4955,6 +5005,14 @@ packages:
protocol-buffers-schema: 3.6.0
dev: true
/resolve-url/0.2.1:
resolution:
{
integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==
}
deprecated: https://github.com/lydell/resolve-url#deprecated
dev: true
/resolve/1.22.2:
resolution:
{
@@ -5567,6 +5625,13 @@ packages:
engines: { node: '>=6' }
dev: true
/tr46/0.0.3:
resolution:
{
integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
}
dev: true
/ts-interface-checker/0.1.13:
resolution:
{
@@ -5724,6 +5789,13 @@ packages:
unist-util-stringify-position: 2.0.3
dev: true
/vite-plugin-cross-origin-isolation/0.1.6:
resolution:
{
integrity: sha512-OY0naW9nPUDrKTffYnY7FRYXOgZdHoNwMGpPxUmj/n32mGZi01fcq+J536jkmwGWX7DLT95XBQVHHbrAJzTvrw==
}
dev: true
/vite-plugin-iso-import/1.0.0_vite@4.2.2:
resolution:
{
@@ -5799,6 +5871,23 @@ packages:
pbf: 3.2.1
dev: true
/webidl-conversions/3.0.1:
resolution:
{
integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
}
dev: true
/whatwg-url/5.0.0:
resolution:
{
integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
}
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
dev: true
/which-module/2.0.0:
resolution:
{
@@ -0,0 +1,56 @@
<!--
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
<script lang="ts">
export let src: string;
let type: 'img' | 'video' | undefined = undefined;
const get_media = async () => {
const res = await fetch(src, { method: 'HEAD' });
const fileType = res.headers.get('Content-Type');
console.log(fileType, 'fileType');
if (fileType.includes('video')) {
console.log('Setting type to video');
type = 'video';
} else {
type = 'img';
console.log(res);
return {
data: URL.createObjectURL(await res.blob()),
alt_text: res.headers.get('X-Alt-Text')
};
}
};
const update_url = () => {
media = get_media();
};
let media = get_media();
$: {
src;
update_url();
}
</script>
{#await media}
<p>Placeholder</p>
{:then data}
{#if type === 'img'}
<img src={data.data} alt={data.alt_text ?? 'Not available'} />
{:else if type === 'video'}
<p>Video!</p>
<video
disablepictureinpicture
disableremoteplayback
x-webkit-airplay="deny"
loop
preload="metadata"
>
<source {src} />
</video>
{:else}
<p>Unknown media type</p>
{/if}
{/await}
+20 -10
View File
@@ -12,14 +12,15 @@
import Spinner from '../Spinner.svelte';
// import { createTippy } from 'svelte-tippy';
import { getLocalization } from '$lib/i18n';
// import MediaComponent from "$lib/editor/MediaComponent.svelte";
const { t } = getLocalization();
/* const tippy = createTippy({
arrow: true,
animation: 'perspective-subtle',
placement: 'top'
});*/
arrow: true,
animation: 'perspective-subtle',
placement: 'top'
});*/
export let data: EditorData;
export let selected_question: number;
@@ -49,6 +50,17 @@
selected_question;
set_unique();
}
let image_url = '';
const update_image_url = () => {
image_url = `/api/v1/storage/download/${data.questions[selected_question].image}`;
console.log('updated!');
};
$: {
update_image_url();
selected_question;
data.questions;
}
/*
if (typeof data.questions[selected_question].type !== QuizQuestionType) {
console.log(data.questions[selected_question].type !== QuizQuestionType.ABCD || data.questions[selected_question].type !== QuizQuestionType.RANGE)
@@ -126,12 +138,9 @@
/>
</svg>
</button>
<img
src="/api/v1/storage/download/{data.questions[selected_question]
.image}"
alt="not available"
class="max-h-64 h-auto w-auto"
/>
{#await import('$lib/editor/MediaComponent.svelte') then c}
<svelte:component this={c.default} bind:src={image_url} />
{/await}
</div>
</div>
{:else}
@@ -144,6 +153,7 @@
bind:edit_id
bind:data
bind:selected_question
video_upload={true}
/>
{/await}
{/if}
@@ -82,6 +82,7 @@
bind:modalOpen={uppyOpen}
bind:edit_id
bind:data
video_upload={false}
/>
{/await}
{/if}
@@ -194,6 +195,7 @@
bind:edit_id
bind:data
selected_question={-1}
video_upload={false}
/>
{/await}
{/if}
+55 -11
View File
@@ -21,6 +21,7 @@
import '@uppy/image-editor/dist/style.css';
import type { EditorData } from '../quiz_types';
import { getLocalization } from '$lib/i18n';
import { onMount } from 'svelte';
const { t } = getLocalization();
@@ -28,6 +29,10 @@
export let edit_id: string;
export let data: EditorData;
export let selected_question: number;
export let video_upload: boolean = false;
// eslint-disable-next-line no-undef
let video_popup: undefined | WindowProxy = undefined;
const uppy = new Uppy()
.use(DropTarget, {
@@ -47,8 +52,9 @@
const props = {
inline: true,
restrictions: {
maxFileSize: 10_000_000,
maxNumberOfFiles: 1
maxFileSize: 10_490_000,
maxNumberOfFiles: 1,
allowedFileTypes: ['image/*']
// allowedFileTypes: ['.gif', '.jpg', '.jpeg', '.png', '.svg', '.webp']
}
};
@@ -69,6 +75,28 @@
modalOpen = false;
});
console.log(edit_id);
onMount(() => {
window.addEventListener('storage', (e) => {
if (e.key !== 'video_upload_id') {
return;
}
localStorage.removeItem('video_upload_id');
data.questions[selected_question].image = e.newValue;
});
});
const upload_video = async () => {
video_popup = window.open(
'/edit/videos',
'_blank',
'popup=true,toolbar=false,menubar=false,location=false,'
);
video_popup.addEventListener('beforeunload', () => {
console.log('Unloaded');
video_popup = undefined;
});
};
</script>
{#if modalOpen}
@@ -77,17 +105,33 @@
transition:fade|local
>
<div>
<button
type="button"
class="rounded-t-lg bg-black text-white px-1"
on:click={() => {
modalOpen = false;
}}
>Close
</button>
<div>
<SvelteDashboard {uppy} width="100%" {props} />
<button
type="button"
class="rounded-t-lg bg-black text-white px-1"
on:click={() => {
modalOpen = false;
}}
>Close
</button>
{#if video_upload}
<button
class="rounded-t-lg bg-black text-white px-1"
on:click={upload_video}
type="button"
>Upload video
</button>
{/if}
</div>
{#if video_popup}
<div class="flex w-full h-1/3 bg-white dark:bg-gray-700 p-4">
<h2 class="text-4xl m-auto">Popup open</h2>
</div>
{:else}
<div>
<SvelteDashboard {uppy} width="100%" {props} />
</div>
{/if}
</div>
</div>
{/if}
@@ -0,0 +1,14 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import type { PageServerLoad } from './$types';
export const load = (async ({ setHeaders }) => {
setHeaders({
'Cross-Origin-Embedder-Policy': 'require-corp',
'Cross-Origin-Opener-Policy': 'same-origin'
});
}) satisfies PageServerLoad;
@@ -0,0 +1,202 @@
<!--
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
<!--
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at https://mozilla.org/MPL/2.0/.
-->
<script lang="ts">
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
import BrownButton from '$lib/components/buttons/brown.svelte';
import { navbarVisible } from '$lib/stores';
let file_input: HTMLInputElement;
navbarVisible.set(false);
let stats: { progress: number; time_elapsed: number; speed: number } = {
progress: 0,
time_elapsed: 0,
speed: 0
};
let upload_stats: { progress: number; upload_started: undefined | Date; time_elapsed: number } =
{
progress: 0,
upload_started: undefined,
time_elapsed: 0
};
const speed_extraction_regex = /speed=(\d+\.\d+)x(?![\s\S]*speed=\d+\.\d+x)/;
let last_time_elapsed = 0;
// eslint-disable-next-line no-unused-vars
enum Status {
// eslint-disable-next-line no-unused-vars
Idle,
// eslint-disable-next-line no-unused-vars
Compressing,
// eslint-disable-next-line no-unused-vars
CompressDone,
// eslint-disable-next-line no-unused-vars
Uploading,
// eslint-disable-next-line no-unused-vars
Done
}
let status: Status = Status.Idle;
let file_size_in_mi: undefined | number = undefined;
let original_file_size_in_mi: undefined | number = undefined;
let file_data: undefined | Blob = undefined;
const compress_video = async () => {
if (status !== Status.Idle) {
return;
}
status = Status.Compressing;
const ffmpeg = createFFmpeg({
// log: true,
progress: (p) => {
if (stats.progress >= 1) {
stats.time_elapsed = last_time_elapsed;
return;
}
stats.time_elapsed = p.time ?? 0;
last_time_elapsed = stats.time_elapsed;
stats.progress = p.ratio ?? 0;
}
});
const file = file_input.files[0];
original_file_size_in_mi = file.size / 1_048_576;
ffmpeg.setLogger(({ message }) => {
const speed_match = message.match(speed_extraction_regex);
stats.speed = speed_match ? parseFloat(speed_match[1]) : 0;
});
await ffmpeg.load();
ffmpeg.FS('writeFile', file.name, await fetchFile(file));
await ffmpeg.run(
'-i',
file.name,
'-vcodec',
'libx264',
'-crf',
'20',
'-vf',
'scale=1080:-2',
'out.mp4'
);
const data = await ffmpeg.FS('readFile', 'out.mp4');
file_size_in_mi = data.length / 1_048_576;
file_data = new Blob([data]);
status = Status.CompressDone;
stats.progress = 1;
};
const upload_video = async () => {
status = Status.Uploading;
upload_stats.upload_started = new Date();
/* const progressTrackingStream = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk);
bytesUploaded += chunk.byteLength;
console.log("upload progress:", bytesUploaded / totalBytes);
upload_stats.progress = bytesUploaded / totalBytes;
upload_stats.time_elapsed = (new Date() - upload_stats.upload_started) / 1000;
},
flush(_controller) {
console.log("completed stream");
}
});*/
/* const response = await fetch("/api/v1/storage/raw", {
method: "POST",
headers: {
"Content-Type": "video/mp4"
},
body: file_data.stream().pipeThrough(progressTrackingStream),
duplex: "half"
});
const json = await response.json()*/
const xhr = new XMLHttpRequest();
const success = await new Promise((resolve) => {
xhr.upload.addEventListener('progress', (event) => {
if (event.lengthComputable) {
upload_stats.progress = event.loaded / event.total;
upload_stats.time_elapsed = (new Date() - upload_stats.upload_started) / 1000;
}
});
xhr.addEventListener('loadend', () => {
resolve({
success: xhr.readyState === 4 && xhr.status === 200,
body: xhr.responseText
});
});
xhr.open('POST', '/api/v1/storage/raw', true);
xhr.setRequestHeader('Content-Type', 'video/mp4');
xhr.send(file_data);
});
console.log('success:', success);
const json = JSON.parse(success.body);
localStorage.setItem('video_upload_id', json.id);
console.log(json, 'done');
status = Status.Done;
};
</script>
<div class="w-screen h-screen m-auto flex flex-col dark:bg-gray-700 p-2 bg-white">
<input type="file" bind:this={file_input} class="m-auto" accept="video/mp4" />
<div class="mt-auto flex justify-center">
{#if status === Status.Compressing || status === Status.CompressDone}
<table>
<tr>
<th>Speed</th>
<th>Progress</th>
<th>Time elapsed</th>
<th>Time remaining</th>
</tr>
<tr>
<td>{stats.speed}</td>
<td>{Math.round(stats.progress * 100)}%</td>
<td>{upload_stats.time_elapsed.toFixed(2)}s</td>
<td
>{(stats.time_elapsed / stats.progress - upload_stats.time_elapsed).toFixed(
2
)}s</td
>
</tr>
</table>
{:else if status === Status.Uploading}
<table>
<tr>
<th>Progress</th>
<th>Time elapsed</th>
<th>Time remaining</th>
</tr>
<tr>
<td>{Math.round(upload_stats.progress * 100)}%</td>
<td>{stats.time_elapsed.toFixed(2)}s</td>
<td>{(stats.time_elapsed / stats.progress - stats.time_elapsed).toFixed(2)}s</td
>
</tr>
</table>
{/if}
</div>
<div class="flex relative my-2">
<div class="w-full">
<BrownButton disabled={stats.progress !== 1} on:click={upload_video}>
Upload {file_size_in_mi ? `${file_size_in_mi.toFixed(2)}Mi` : ''}</BrownButton
>
</div>
<span
class="h-full transition-all absolute rounded bg-black bg-opacity-50"
style="width: {100 - stats.progress * 100}%"
/>
</div>
<div class="flex justify-center">
<BrownButton on:click={compress_video} disabled={status !== Status.Idle}>Submit</BrownButton
>
</div>
</div>
+16 -2
View File
@@ -1,8 +1,22 @@
import { sveltekit } from '@sveltejs/kit/vite';
import crossOriginIsolation from 'vite-plugin-cross-origin-isolation';
/** @type {import('vite').UserConfig} */
/** @type {import("vite").UserConfig} */
const config = {
plugins: [sveltekit()],
plugins: [
sveltekit(),
{
name: 'configure-response-headers',
configureServer: (server) => {
server.middlewares.use((_req, res, next) => {
/* res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
res.setHeader("Access-Control-Allow-Origin", "https://ncs3.classquiz.de");*/
next();
});
}
}
],
server: {
port: 3000
},