224 lines
5.7 KiB
Svelte
224 lines
5.7 KiB
Svelte
<script lang="ts">
|
|
import {onMount} from "svelte";
|
|
import Icon from "../utils/Icon.svelte"
|
|
|
|
export let file: File;
|
|
export let form: HTMLFormElement;
|
|
export let fileFieldName: string = 'file';
|
|
export let autostart: boolean = false;
|
|
|
|
export let inProgress: boolean = false;
|
|
let xhr: XMLHttpRequest | undefined;
|
|
let xferSpeed: number[] = [];
|
|
let lastTransferTime: number | null = null;
|
|
let lastLoaded: number = 0;
|
|
let speed: string | undefined;
|
|
|
|
export let progress: string = '';
|
|
export let error: string | undefined = undefined;
|
|
export let finalUrl: string | undefined = undefined;
|
|
|
|
$: file && autoStart();
|
|
|
|
const units = ['K', 'M', 'G', 'T'];
|
|
|
|
export async function run(): Promise<void> {
|
|
inProgress = true;
|
|
try {
|
|
await new Promise<void>((resolve, reject) => {
|
|
reset();
|
|
prepareXhrRequest(resolve, reject);
|
|
if (!xhr) throw new Error('Failed to initialize xhr');
|
|
sendXhrRequest(xhr);
|
|
});
|
|
} catch (e) {
|
|
error = e;
|
|
return onError(e);
|
|
} finally {
|
|
inProgress = false;
|
|
}
|
|
|
|
onEnd(file.name, finalUrl);
|
|
}
|
|
|
|
function retry() {
|
|
run().catch(onError);
|
|
}
|
|
|
|
function cancel() {
|
|
onEnd(file.name, null);
|
|
}
|
|
|
|
function reset() {
|
|
resetResult();
|
|
initSpeedTracker();
|
|
initProgressTracker();
|
|
initErrorTracker();
|
|
}
|
|
|
|
function resetResult() {
|
|
finalUrl = undefined;
|
|
}
|
|
|
|
function initSpeedTracker() {
|
|
xferSpeed = [];
|
|
lastTransferTime = null;
|
|
lastLoaded = 0;
|
|
speed = '--';
|
|
}
|
|
|
|
function initProgressTracker() {
|
|
progress = '';
|
|
}
|
|
|
|
function initErrorTracker() {
|
|
error = undefined;
|
|
}
|
|
|
|
function prepareXhrRequest(
|
|
resolve: (value?: PromiseLike<void>) => void,
|
|
reject: (err: string | ProgressEvent<XMLHttpRequestEventTarget> | undefined) => void,
|
|
) {
|
|
xhr = new XMLHttpRequest();
|
|
|
|
xhr.responseType = 'json';
|
|
xhr.upload.addEventListener('progress', e => {
|
|
if (e.lengthComputable) {
|
|
progress = (e.loaded / e.total * 100).toFixed(2) + '%';
|
|
updateSpeed(e.loaded);
|
|
} else {
|
|
progress = '';
|
|
}
|
|
});
|
|
xhr.addEventListener('load', () => {
|
|
const response = xhr.response;
|
|
if (response) {
|
|
console.log('done', response);
|
|
|
|
if (response.status === 'error') {
|
|
if (response.messages) {
|
|
// TODO: display errors // applyFormMessages(this.form, response.messages);
|
|
const messages = response.messages;
|
|
const formattedError = Object.keys(messages).map(field => `${field}: ${messages[field].name}, ${messages[field].message}`).join(';');
|
|
return reject(formattedError);
|
|
}
|
|
} else if (response.url) {
|
|
finalUrl = response.url;
|
|
return resolve();
|
|
}
|
|
|
|
reject('Malformed response from server.');
|
|
}
|
|
});
|
|
xhr.addEventListener('error', (e) => {
|
|
reject(e);
|
|
});
|
|
}
|
|
|
|
function sendXhrRequest(xhr: XMLHttpRequest) {
|
|
const formData = new FormData(form);
|
|
formData.set(fileFieldName, file);
|
|
xhr.open('POST', form.action);
|
|
xhr.send(formData);
|
|
}
|
|
|
|
function updateSpeed(loaded: number): void {
|
|
const time = new Date().getTime();
|
|
if (lastTransferTime) {
|
|
xferSpeed.push((loaded - lastLoaded) / (time - lastTransferTime));
|
|
|
|
if (xferSpeed.length > 100) xferSpeed = xferSpeed.slice(1);
|
|
|
|
let rawSpeed = xferSpeed.reduce((v, c) => v + c) / xferSpeed.length;
|
|
let unit = 0;
|
|
while (rawSpeed >= 1000 && unit < units.length - 1) {
|
|
rawSpeed /= 1000;
|
|
unit++;
|
|
}
|
|
|
|
speed = rawSpeed.toFixed(2) + units[unit] + 'Bps';
|
|
}
|
|
lastTransferTime = time;
|
|
lastLoaded = loaded;
|
|
}
|
|
|
|
onMount(() => {
|
|
autoStart();
|
|
});
|
|
|
|
function autoStart() {
|
|
if (autostart && !inProgress) {
|
|
run().catch(onError);
|
|
}
|
|
}
|
|
|
|
export let onEnd = function (name: string, finalUrl: string) {
|
|
}
|
|
|
|
export let onError = function (err) {
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss">
|
|
@import "../../scss/vars";
|
|
|
|
.file-upload {
|
|
padding: 8px;
|
|
color: var(--on-subsurface);
|
|
background: var(--subsurface);
|
|
border-radius: 5px;
|
|
|
|
.name, .status {
|
|
text-align: center;
|
|
}
|
|
|
|
&.error {
|
|
color: var(--on-error);
|
|
background-color: var(--error);
|
|
}
|
|
}
|
|
|
|
.progress-bar {
|
|
position: relative;
|
|
display: block;
|
|
margin: 8px;
|
|
padding: 4px;
|
|
background: #fff1;
|
|
border-radius: 5px;
|
|
overflow: hidden;
|
|
text-align: center;
|
|
|
|
.content {
|
|
position: relative;
|
|
}
|
|
|
|
&::before {
|
|
content: "";
|
|
display: block;
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
width: var(--progress);
|
|
height: 100%;
|
|
transition: width ease-out 150ms;
|
|
|
|
background: var(--primary);
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="file-upload" class:error={!!error}>
|
|
<div class="name">{file?.name}</div>
|
|
<div class="progress-bar" style="--progress: {progress};"><span class="content">{progress}</span></div>
|
|
<div class="status">
|
|
{#if error}
|
|
Error: {error}
|
|
|
|
<button on:click={retry}><Icon name="repeat"/> Retry</button>
|
|
<button on:click={cancel}><Icon name="slash"/> Cancel</button>
|
|
{:else}
|
|
Uploading @ {speed}
|
|
{/if}
|
|
</div>
|
|
</div>
|