feat(webui,jslib): upload progress
parent
59c2d9c078
commit
ec81f8147b
|
@ -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(
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const browser = typeof window !== "undefined";
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue