feat(webui,jslib): upload progress

feat/tables
Tomáš Mládek 2024-02-05 22:04:48 +01:00
parent 59c2d9c078
commit ec81f8147b
5 changed files with 108 additions and 26 deletions

View File

@ -2,8 +2,8 @@ import LRU from "lru-cache";
import type { Query, UpObject } from ".";
import { UpListing } from ".";
import type {
ADDRESS_TYPE,
Address,
ADDRESS_TYPE,
AttributeListingResult,
EntityListing,
IJob,
@ -14,8 +14,10 @@ import type {
StoreInfo,
VaultInfo,
} from "./types";
import type { UpEndWasmExtensions, AddressComponents } from "./wasm";
import type { AddressComponents, UpEndWasmExtensions } from "./wasm";
import debug from "debug";
import { browser } from "./util";
const dbg = debug("upend:api");
export type { AddressComponents };
@ -152,28 +154,67 @@ export class UpEndApi {
public async putBlob(
fileOrUrl: File | URL,
options?: ApiFetchOptions,
options?: ApiFetchOptions & { onProgress?: (ev: ProgressEvent) => void },
): Promise<PutResult> {
dbg("Putting Blob: %O", fileOrUrl);
const formData = new FormData();
if (fileOrUrl instanceof File) {
formData.append(fileOrUrl.name, fileOrUrl);
} else {
formData.append("@url", fileOrUrl.toString());
}
const signal = this.getAbortSignal(options);
const response = await fetch(`${this.apiUrl}/blob`, {
method: "PUT",
body: formData,
signal,
});
if (!response.ok) {
throw Error(await response.text());
if (browser && fileOrUrl instanceof File) {
dbg("Using XHR for file upload");
const xhrdbg = debug("upend:api:xhr");
const xhr = new XMLHttpRequest();
signal.addEventListener("abort", () => xhr.abort());
for (const event of [
"loadstart",
"load",
"loadend",
"progress",
"abort",
"error",
] as const) {
xhr.addEventListener(event, (ev) => xhrdbg(`XHR ${event}: %O`, ev));
xhr.upload.addEventListener(event, (ev) =>
xhrdbg(`XHR upload ${event}: %O`, ev),
);
if (options?.onProgress) {
xhr.upload.addEventListener(event, options.onProgress);
}
}
return new Promise((resolve, reject) => {
xhr.open("PUT", `${this.apiUrl}/blob`, true);
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
resolve(JSON.parse(xhr.responseText));
} catch (e) {
reject(e);
}
} else {
reject(xhr.statusText);
}
};
xhr.send(formData);
});
} else {
const response = await fetch(`${this.apiUrl}/blob`, {
method: "PUT",
body: formData,
signal,
});
if (!response.ok) {
throw Error(await response.text());
}
return await response.json();
}
return await response.json();
}
public async deleteEntry(

View File

@ -1,6 +1,6 @@
{
"name": "@upnd/upend",
"version": "0.3.3",
"version": "0.4.0",
"description": "Client library to interact with the UpEnd system.",
"scripts": {
"build": "tsc --build --verbose",

1
tools/upend_js/util.ts Normal file
View File

@ -0,0 +1 @@
export const browser = typeof window !== "undefined";

View File

@ -11,13 +11,18 @@
<script lang="ts">
import Icon from './utils/Icon.svelte';
import IconButton from './utils/IconButton.svelte';
import ProgressBar from './utils/ProgressBar.svelte';
import api from '$lib/api';
import { goto } from '$app/navigation';
import { i18n } from '$lib/i18n';
let files: File[] = [];
let URLs: string[] = [];
let uploading = false;
let progress: Record<string, number> = {};
let totalProgress: number | undefined;
$: visible = files.length + URLs.length > 0;
addEmitter.on('files', (ev) => {
@ -33,7 +38,17 @@
uploading = true;
try {
const addresses = await Promise.all(files.map(async (file) => api.putBlob(file)));
const addresses = [];
for (const file of files) {
const address = await api.putBlob(file, {
onProgress: (p) => {
progress[file.name] = (p.loaded / p.total) * 100;
totalProgress = Object.values(progress).reduce((a, b) => a + b, 0) / files.length;
},
timeout: -1
});
addresses.push(address);
}
goto(`/browse/${addresses.join(',')}`);
} catch (error) {
@ -48,6 +63,7 @@
if (!uploading) {
files = [];
URLs = [];
progress = {};
}
}
</script>
@ -59,21 +75,34 @@
<div class="addmodal" on:click|stopPropagation>
<div class="files">
{#each files as file}
<div class="file">
{#if file.type.startsWith('image')}
<img src={URL.createObjectURL(file)} alt="To be uploaded." />
{:else}
<div class="add">
<div class="file">
<div class="icon">
<Icon name="file" />
</div>
<div class="label">{file.name}</div>
{#if file.type.startsWith('image')}
<img src={URL.createObjectURL(file)} alt="To be uploaded." />
{/if}
</div>
{#if uploading && files.length > 1}
<div class="progress">
<ProgressBar value={progress[file.name] || 0} />
</div>
{/if}
<div class="label">{file.name}</div>
</div>
{/each}
</div>
<div class="controls">
<IconButton name="upload" on:click={upload} />
<IconButton small disabled={uploading} name="upload" on:click={upload}>
{$i18n.t('Upload')}
</IconButton>
</div>
{#if uploading}
<div class="progress">
<ProgressBar value={totalProgress} />
</div>
{/if}
</div>
</div>
@ -88,6 +117,7 @@
color: var(--foreground);
display: none;
&.visible {
display: unset;
}
@ -96,7 +126,7 @@
cursor: progress;
.addmodal {
filter: brightness(0.5);
filter: brightness(0.85);
}
}
}
@ -113,6 +143,7 @@
border: solid 2px var(--foreground);
border-radius: 8px;
padding: 1rem;
min-width: 33vw;
}
.files {
@ -124,12 +155,13 @@
overflow-y: auto;
max-height: 66vh;
width: 80vw;
}
.file {
display: flex;
align-items: center;
flex-direction: column;
gap: 1rem;
border: 1px solid var(--foreground);
border-radius: 4px;
@ -147,7 +179,6 @@
.label {
flex-grow: 1;
text-align: center;
}
}
@ -157,4 +188,13 @@
font-size: 48px;
margin-top: 0.5rem;
}
.progress {
margin-top: 1rem;
margin-bottom: 0.5rem;
}
.add .progress {
margin-bottom: 1.5rem;
}
</style>

View File

@ -2,11 +2,11 @@
export let value: number | undefined = undefined;
</script>
<div class="progress-bar" class:indeterminate={value == undefined} style="--value: {value}%">
<div class="progress-bar" class:indeterminate={value === undefined} style="--value: {value}%">
<div class="value" />
<div class="label">
<slot>
{value ? Math.round(value) : '?'}%
{typeof value === 'number' ? Math.round(value) : '?'}%
</slot>
</div>
</div>