ily.li/src/assets/views/components/FileUpload.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>