mirror of
https://github.com/immich-app/immich.git
synced 2025-01-27 22:22:45 +01:00
refactor(web): upload panel (#12326)
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
0d6bef2c05
commit
f4ec842577
6 changed files with 184 additions and 140 deletions
web/src/lib
components
models
stores
utils
|
@ -16,13 +16,14 @@
|
||||||
export let ariaLabelledby: string | undefined = undefined;
|
export let ariaLabelledby: string | undefined = undefined;
|
||||||
export let strokeWidth: number = 0;
|
export let strokeWidth: number = 0;
|
||||||
export let strokeColor: string = 'currentColor';
|
export let strokeColor: string = 'currentColor';
|
||||||
|
export let spin = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
{viewBox}
|
{viewBox}
|
||||||
class="{className} {flipped ? '-scale-x-100' : ''}"
|
class="{className} {flipped ? '-scale-x-100' : ''} {spin ? 'animate-spin' : ''}"
|
||||||
{role}
|
{role}
|
||||||
stroke={strokeColor}
|
stroke={strokeColor}
|
||||||
stroke-width={strokeWidth}
|
stroke-width={strokeWidth}
|
||||||
|
|
|
@ -1,21 +1,32 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
import { AppRoute } from '$lib/constants';
|
||||||
import type { UploadAsset } from '$lib/models/upload-asset';
|
import type { UploadAsset } from '$lib/models/upload-asset';
|
||||||
import { UploadState } from '$lib/models/upload-asset';
|
import { UploadState } from '$lib/models/upload-asset';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
import ImmichLogo from './immich-logo.svelte';
|
|
||||||
import { getFilenameExtension } from '$lib/utils/asset-utils';
|
|
||||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||||
import { fileUploadHandler } from '$lib/utils/file-uploader';
|
import { fileUploadHandler } from '$lib/utils/file-uploader';
|
||||||
import { mdiRefresh, mdiCancel } from '@mdi/js';
|
import {
|
||||||
|
mdiAlertCircle,
|
||||||
|
mdiCheckCircle,
|
||||||
|
mdiCircleOutline,
|
||||||
|
mdiClose,
|
||||||
|
mdiLoading,
|
||||||
|
mdiOpenInNew,
|
||||||
|
mdiRestart,
|
||||||
|
} from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
export let uploadAsset: UploadAsset;
|
export let uploadAsset: UploadAsset;
|
||||||
|
|
||||||
|
const handleDismiss = (uploadAsset: UploadAsset) => {
|
||||||
|
uploadAssetsStore.removeItem(uploadAsset.id);
|
||||||
|
};
|
||||||
|
|
||||||
const handleRetry = async (uploadAsset: UploadAsset) => {
|
const handleRetry = async (uploadAsset: UploadAsset) => {
|
||||||
uploadAssetsStore.removeUploadAsset(uploadAsset.id);
|
uploadAssetsStore.removeItem(uploadAsset.id);
|
||||||
await fileUploadHandler([uploadAsset.file], uploadAsset.albumId);
|
await fileUploadHandler([uploadAsset.file], uploadAsset.albumId);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -23,86 +34,69 @@
|
||||||
<div
|
<div
|
||||||
in:fade={{ duration: 250 }}
|
in:fade={{ duration: 250 }}
|
||||||
out:fade={{ duration: 100 }}
|
out:fade={{ duration: 100 }}
|
||||||
class="flex flex-col rounded-lg bg-immich-bg text-xs dark:bg-immich-dark-bg"
|
class="flex flex-col rounded-lg bg-immich-bg text-xs dark:bg-immich-dark-bg p-2 gap-1"
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-[65px_auto_auto] max-h-[70px]">
|
<div class="flex items-center gap-2">
|
||||||
<div class="relative">
|
<div class="flex items-center justify-center">
|
||||||
<div in:fade={{ duration: 250 }}>
|
{#if uploadAsset.state === UploadState.PENDING}
|
||||||
<ImmichLogo noText class="h-[65px] w-[65px] rounded-bl-lg rounded-tl-lg object-cover p-2" />
|
<Icon path={mdiCircleOutline} size="24" class="text-immich-primary" title={$t('pending')} />
|
||||||
</div>
|
{:else if uploadAsset.state === UploadState.STARTED}
|
||||||
<div class="absolute bottom-0 left-0 h-[25px] w-full rounded-bl-md bg-immich-primary/30">
|
<Icon path={mdiLoading} size="24" spin class="text-immich-primary" title={$t('asset_skipped')} />
|
||||||
<p
|
{:else if uploadAsset.state === UploadState.ERROR}
|
||||||
class="absolute bottom-1 right-1 stroke-immich-primary object-right-bottom font-semibold uppercase text-white/95 dark:text-gray-100"
|
<Icon path={mdiAlertCircle} size="24" class="text-immich-error" title={$t('error')} />
|
||||||
>
|
{:else if uploadAsset.state === UploadState.DUPLICATED}
|
||||||
.{getFilenameExtension(uploadAsset.file.name)}
|
<Icon path={mdiAlertCircle} size="24" class="text-immich-warning" title={$t('asset_skipped')} />
|
||||||
</p>
|
{:else if uploadAsset.state === UploadState.DONE}
|
||||||
</div>
|
<Icon path={mdiCheckCircle} size="24" class="text-immich-success" title={$t('asset_uploaded')} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-between p-2 pr-2">
|
<!-- <span>[{getByteUnitString(uploadAsset.file.size, $locale)}]</span> -->
|
||||||
<input
|
<span class="grow break-all">{uploadAsset.file.name}</span>
|
||||||
disabled
|
|
||||||
class="w-full rounded-md border bg-gray-100 p-1 px-2 text-[10px] dark:border-immich-dark-gray dark:bg-gray-900"
|
|
||||||
value={`[${getByteUnitString(uploadAsset.file.size, $locale)}] ${uploadAsset.file.name}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
{#if uploadAsset.state === UploadState.DUPLICATED && uploadAsset.assetId}
|
||||||
class="relative mt-[5px] h-[15px] w-full rounded-md bg-gray-300 text-white dark:bg-immich-dark-gray"
|
<div class="flex items-center justify-between gap-1">
|
||||||
class:dark:text-black={uploadAsset.state === UploadState.STARTED}
|
<a
|
||||||
>
|
href="{AppRoute.PHOTOS}/{uploadAsset.assetId}"
|
||||||
{#if uploadAsset.state === UploadState.STARTED}
|
target="_blank"
|
||||||
<div class="h-[15px] rounded-md bg-immich-primary transition-all" style={`width: ${uploadAsset.progress}%`} />
|
rel="noopener noreferrer"
|
||||||
<p class="absolute top-0 h-full w-full text-center text-[10px]">
|
class=""
|
||||||
{#if uploadAsset.message}
|
aria-hidden="true"
|
||||||
{uploadAsset.message}
|
tabindex={-1}
|
||||||
{:else}
|
|
||||||
{uploadAsset.progress}% - {getByteUnitString(uploadAsset.speed || 0, $locale)}/s - {uploadAsset.eta}s
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
{:else if uploadAsset.state === UploadState.PENDING}
|
|
||||||
<div class="h-[15px] rounded-md bg-immich-dark-gray transition-all dark:bg-immich-gray" style="width: 100%" />
|
|
||||||
<p class="absolute top-0 h-full w-full text-center text-[10px]">{$t('pending')}</p>
|
|
||||||
{:else if uploadAsset.state === UploadState.ERROR}
|
|
||||||
<div class="h-[15px] rounded-md bg-immich-error transition-all" style="width: 100%" />
|
|
||||||
<p class="absolute top-0 h-full w-full text-center text-[10px]">{$t('error')}</p>
|
|
||||||
{:else if uploadAsset.state === UploadState.DUPLICATED}
|
|
||||||
<div class="h-[15px] rounded-md bg-immich-warning transition-all" style="width: 100%" />
|
|
||||||
<p class="absolute top-0 h-full w-full text-center text-[10px]">
|
|
||||||
{$t('asset_skipped')}
|
|
||||||
{#if uploadAsset.message}
|
|
||||||
({uploadAsset.message})
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
{:else if uploadAsset.state === UploadState.DONE}
|
|
||||||
<div class="h-[15px] rounded-md bg-immich-success transition-all" style="width: 100%" />
|
|
||||||
<p class="absolute top-0 h-full w-full text-center text-[10px]">
|
|
||||||
{$t('asset_uploaded')}
|
|
||||||
{#if uploadAsset.message}
|
|
||||||
({uploadAsset.message})
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if uploadAsset.state === UploadState.ERROR}
|
|
||||||
<div class="flex h-full flex-col place-content-evenly place-items-center justify-items-center pr-2">
|
|
||||||
<button type="button" on:click={() => handleRetry(uploadAsset)} title={$t('retry_upload')} class="flex text-sm">
|
|
||||||
<span class="text-immich-dark-gray dark:text-immich-dark-fg"><Icon path={mdiRefresh} size="20" /></span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
on:click={() => uploadAssetsStore.removeUploadAsset(uploadAsset.id)}
|
|
||||||
title={$t('dismiss_error')}
|
|
||||||
class="flex text-sm"
|
|
||||||
>
|
>
|
||||||
<span class="text-immich-error"><Icon path={mdiCancel} size="20" /></span>
|
<Icon path={mdiOpenInNew} size="20" />
|
||||||
|
</a>
|
||||||
|
<button type="button" on:click={() => handleDismiss(uploadAsset)} class="" aria-hidden="true" tabindex={-1}>
|
||||||
|
<Icon path={mdiClose} size="20" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else if uploadAsset.state === UploadState.ERROR}
|
||||||
|
<div class="flex items-center justify-between gap-1">
|
||||||
|
<button type="button" on:click={() => handleRetry(uploadAsset)} class="" aria-hidden="true" tabindex={-1}>
|
||||||
|
<Icon path={mdiRestart} size="20" />
|
||||||
|
</button>
|
||||||
|
<button type="button" on:click={() => handleDismiss(uploadAsset)} class="" aria-hidden="true" tabindex={-1}>
|
||||||
|
<Icon path={mdiClose} size="20" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if uploadAsset.state === UploadState.STARTED}
|
||||||
|
<div class="text-black relative mt-[5px] h-[15px] w-full rounded-md bg-gray-300 dark:bg-immich-dark-gray">
|
||||||
|
<div class="h-[15px] rounded-md bg-immich-primary transition-all" style={`width: ${uploadAsset.progress}%`} />
|
||||||
|
<p class="absolute top-0 h-full w-full text-center text-[10px]">
|
||||||
|
{#if uploadAsset.message}
|
||||||
|
{uploadAsset.message}
|
||||||
|
{:else}
|
||||||
|
{uploadAsset.progress}% - {getByteUnitString(uploadAsset.speed || 0, $locale)}/s - {uploadAsset.eta}s
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if uploadAsset.state === UploadState.ERROR}
|
{#if uploadAsset.state === UploadState.ERROR}
|
||||||
<div class="flex flex-row justify-between">
|
<div class="flex flex-row justify-between">
|
||||||
<p class="w-full rounded-md py-1 px-2 text-justify text-[10px] text-immich-error">
|
<p class="w-full rounded-md text-justify text-immich-error">
|
||||||
{uploadAsset.error}
|
{uploadAsset.error}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,8 +15,7 @@
|
||||||
let showOptions = false;
|
let showOptions = false;
|
||||||
let concurrency = uploadExecutionQueue.concurrency;
|
let concurrency = uploadExecutionQueue.concurrency;
|
||||||
|
|
||||||
let { isUploading, hasError, remainingUploads, errorCounter, duplicateCounter, successCounter, totalUploadCounter } =
|
let { stats, isDismissible, isUploading, remainingUploads } = uploadAssetsStore;
|
||||||
uploadAssetsStore;
|
|
||||||
|
|
||||||
const autoHide = () => {
|
const autoHide = () => {
|
||||||
if (!$isUploading && showDetail) {
|
if (!$isUploading && showDetail) {
|
||||||
|
@ -33,29 +32,29 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $hasError || $isUploading}
|
{#if $isUploading}
|
||||||
<div
|
<div
|
||||||
in:fade={{ duration: 250 }}
|
in:fade={{ duration: 250 }}
|
||||||
out:fade={{ duration: 250 }}
|
out:fade={{ duration: 250 }}
|
||||||
on:outroend={() => {
|
on:outroend={() => {
|
||||||
if ($errorCounter > 0) {
|
if ($stats.errors > 0) {
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: $t('upload_errors', { values: { count: $errorCounter } }),
|
message: $t('upload_errors', { values: { count: $stats.errors } }),
|
||||||
type: NotificationType.Warning,
|
type: NotificationType.Warning,
|
||||||
});
|
});
|
||||||
} else if ($successCounter > 0) {
|
} else if ($stats.success > 0) {
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: $t('upload_success'),
|
message: $t('upload_success'),
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if ($duplicateCounter > 0) {
|
if ($stats.duplicates > 0) {
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: $t('upload_skipped_duplicates', { values: { count: $duplicateCounter } }),
|
message: $t('upload_skipped_duplicates', { values: { count: $stats.duplicates } }),
|
||||||
type: NotificationType.Warning,
|
type: NotificationType.Warning,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
uploadAssetsStore.resetStore();
|
uploadAssetsStore.reset();
|
||||||
}}
|
}}
|
||||||
class="fixed bottom-6 right-6 z-[10000]"
|
class="fixed bottom-6 right-6 z-[10000]"
|
||||||
>
|
>
|
||||||
|
@ -70,20 +69,20 @@
|
||||||
{$t('upload_progress', {
|
{$t('upload_progress', {
|
||||||
values: {
|
values: {
|
||||||
remaining: $remainingUploads,
|
remaining: $remainingUploads,
|
||||||
processed: $successCounter + $errorCounter,
|
processed: $stats.total - $remainingUploads,
|
||||||
total: $totalUploadCounter,
|
total: $stats.total,
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<p class="immich-form-label text-xs">
|
<p class="immich-form-label text-xs">
|
||||||
{$t('upload_status_uploaded')}
|
{$t('upload_status_uploaded')}
|
||||||
<span class="text-immich-success">{$successCounter.toLocaleString($locale)}</span>
|
<span class="text-immich-success">{$stats.success.toLocaleString($locale)}</span>
|
||||||
-
|
-
|
||||||
{$t('upload_status_errors')}
|
{$t('upload_status_errors')}
|
||||||
<span class="text-immich-error">{$errorCounter.toLocaleString($locale)}</span>
|
<span class="text-immich-error">{$stats.errors.toLocaleString($locale)}</span>
|
||||||
-
|
-
|
||||||
{$t('upload_status_duplicates')}
|
{$t('upload_status_duplicates')}
|
||||||
<span class="text-immich-warning">{$duplicateCounter.toLocaleString($locale)}</span>
|
<span class="text-immich-warning">{$stats.duplicates.toLocaleString($locale)}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-end">
|
<div class="flex flex-col items-end">
|
||||||
|
@ -103,7 +102,7 @@
|
||||||
on:click={() => (showDetail = false)}
|
on:click={() => (showDetail = false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if $hasError}
|
{#if $isDismissible}
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
title={$t('dismiss_all_errors')}
|
title={$t('dismiss_all_errors')}
|
||||||
icon={mdiCancel}
|
icon={mdiCancel}
|
||||||
|
@ -115,7 +114,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if showOptions}
|
{#if showOptions}
|
||||||
<div class="immich-scrollbar mb-4 max-h-[400px] overflow-y-auto rounded-lg pr-2">
|
<div class="immich-scrollbar mb-4 max-h-[400px] overflow-y-auto rounded-lg">
|
||||||
<div class="flex h-[26px] place-items-center gap-1">
|
<div class="flex h-[26px] place-items-center gap-1">
|
||||||
<label class="immich-form-label" for="upload-concurrency">{$t('upload_concurrency')}</label>
|
<label class="immich-form-label" for="upload-concurrency">{$t('upload_concurrency')}</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -133,7 +132,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="immich-scrollbar flex max-h-[400px] flex-col gap-2 overflow-y-auto rounded-lg pr-2">
|
<div class="immich-scrollbar flex max-h-[400px] flex-col gap-2 overflow-y-auto rounded-lg">
|
||||||
{#each $uploadAssetsStore as uploadAsset (uploadAsset.id)}
|
{#each $uploadAssetsStore as uploadAsset (uploadAsset.id)}
|
||||||
<UploadAssetPreview {uploadAsset} />
|
<UploadAssetPreview {uploadAsset} />
|
||||||
{/each}
|
{/each}
|
||||||
|
@ -149,14 +148,14 @@
|
||||||
>
|
>
|
||||||
{$remainingUploads.toLocaleString($locale)}
|
{$remainingUploads.toLocaleString($locale)}
|
||||||
</button>
|
</button>
|
||||||
{#if $hasError}
|
{#if $stats.errors > 0}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
in:scale={{ duration: 250, easing: quartInOut }}
|
in:scale={{ duration: 250, easing: quartInOut }}
|
||||||
on:click={() => (showDetail = true)}
|
on:click={() => (showDetail = true)}
|
||||||
class="absolute -right-4 -top-4 flex h-10 w-10 place-content-center place-items-center rounded-full bg-immich-error p-5 text-xs text-gray-200"
|
class="absolute -right-4 -top-4 flex h-10 w-10 place-content-center place-items-center rounded-full bg-immich-error p-5 text-xs text-gray-200"
|
||||||
>
|
>
|
||||||
{$errorCounter.toLocaleString($locale)}
|
{$stats.errors.toLocaleString($locale)}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -9,8 +9,8 @@ export enum UploadState {
|
||||||
export type UploadAsset = {
|
export type UploadAsset = {
|
||||||
id: string;
|
id: string;
|
||||||
file: File;
|
file: File;
|
||||||
albumId?: string;
|
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
|
albumId?: string;
|
||||||
progress?: number;
|
progress?: number;
|
||||||
state?: UploadState;
|
state?: UploadState;
|
||||||
startDate?: number;
|
startDate?: number;
|
||||||
|
|
|
@ -3,32 +3,36 @@ import { UploadState, type UploadAsset } from '../models/upload-asset';
|
||||||
|
|
||||||
function createUploadStore() {
|
function createUploadStore() {
|
||||||
const uploadAssets = writable<Array<UploadAsset>>([]);
|
const uploadAssets = writable<Array<UploadAsset>>([]);
|
||||||
|
const stats = writable<{ errors: number; duplicates: number; success: number; total: number }>({
|
||||||
const duplicateCounter = writable(0);
|
errors: 0,
|
||||||
const successCounter = writable(0);
|
duplicates: 0,
|
||||||
const totalUploadCounter = writable(0);
|
success: 0,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const { subscribe } = uploadAssets;
|
const { subscribe } = uploadAssets;
|
||||||
|
|
||||||
const isUploading = derived(uploadAssets, ($uploadAssets) => {
|
const isUploading = derived(uploadAssets, (items) => items.length > 0);
|
||||||
return $uploadAssets.length > 0;
|
const isDismissible = derived(uploadAssets, (items) =>
|
||||||
});
|
items.some((item) => item.state === UploadState.ERROR || item.state === UploadState.DUPLICATED),
|
||||||
const errorsAssets = derived(uploadAssets, (a) => a.filter((e) => e.state === UploadState.ERROR));
|
);
|
||||||
const errorCounter = derived(errorsAssets, (values) => values.length);
|
|
||||||
const hasError = derived(errorCounter, (values) => values > 0);
|
|
||||||
const remainingUploads = derived(
|
const remainingUploads = derived(
|
||||||
uploadAssets,
|
uploadAssets,
|
||||||
(values) => values.filter((a) => a.state === UploadState.PENDING || a.state === UploadState.STARTED).length,
|
(values) => values.filter((a) => a.state === UploadState.PENDING || a.state === UploadState.STARTED).length,
|
||||||
);
|
);
|
||||||
|
|
||||||
const addNewUploadAsset = (newAsset: UploadAsset) => {
|
const addItem = (newAsset: UploadAsset) => {
|
||||||
uploadAssets.update(($assets) => {
|
uploadAssets.update(($assets) => {
|
||||||
const duplicate = $assets.find((asset) => asset.id === newAsset.id);
|
const duplicate = $assets.find((asset) => asset.id === newAsset.id);
|
||||||
if (duplicate) {
|
if (duplicate) {
|
||||||
return $assets.map((asset) => (asset.id === newAsset.id ? newAsset : asset));
|
return $assets.map((asset) => (asset.id === newAsset.id ? newAsset : asset));
|
||||||
}
|
}
|
||||||
|
|
||||||
totalUploadCounter.update((c) => c + 1);
|
stats.update((stats) => {
|
||||||
|
stats.total++;
|
||||||
|
return stats;
|
||||||
|
});
|
||||||
|
|
||||||
$assets.push({
|
$assets.push({
|
||||||
...newAsset,
|
...newAsset,
|
||||||
speed: 0,
|
speed: 0,
|
||||||
|
@ -36,6 +40,7 @@ function createUploadStore() {
|
||||||
progress: 0,
|
progress: 0,
|
||||||
eta: 0,
|
eta: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
return $assets;
|
return $assets;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -53,7 +58,7 @@ function createUploadStore() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const markStarted = (id: string) => {
|
const markStarted = (id: string) => {
|
||||||
updateAsset(id, {
|
updateItem(id, {
|
||||||
state: UploadState.STARTED,
|
state: UploadState.STARTED,
|
||||||
startDate: Date.now(),
|
startDate: Date.now(),
|
||||||
});
|
});
|
||||||
|
@ -70,39 +75,61 @@ function createUploadStore() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateAsset = (id: string, partialObject: Partial<UploadAsset>) => {
|
const updateItem = (id: string, partialObject: Partial<UploadAsset>) => {
|
||||||
updateAssetMap(id, (v) => ({ ...v, ...partialObject }));
|
updateAssetMap(id, (v) => ({ ...v, ...partialObject }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeUploadAsset = (id: string) => {
|
const removeItem = (id: string) => {
|
||||||
uploadAssets.update((uploadingAsset) => uploadingAsset.filter((a) => a.id != id));
|
uploadAssets.update((uploadingAsset) => uploadingAsset.filter((a) => a.id != id));
|
||||||
};
|
};
|
||||||
|
|
||||||
const dismissErrors = () => uploadAssets.update((value) => value.filter((e) => e.state !== UploadState.ERROR));
|
const dismissErrors = () =>
|
||||||
|
uploadAssets.update((value) =>
|
||||||
|
value.filter((e) => e.state !== UploadState.ERROR && e.state !== UploadState.DUPLICATED),
|
||||||
|
);
|
||||||
|
|
||||||
const resetStore = () => {
|
const reset = () => {
|
||||||
uploadAssets.set([]);
|
uploadAssets.set([]);
|
||||||
duplicateCounter.set(0);
|
stats.set({ errors: 0, duplicates: 0, success: 0, total: 0 });
|
||||||
successCounter.set(0);
|
};
|
||||||
totalUploadCounter.set(0);
|
|
||||||
|
const track = (value: 'success' | 'duplicate' | 'error') => {
|
||||||
|
stats.update((stats) => {
|
||||||
|
switch (value) {
|
||||||
|
case 'success': {
|
||||||
|
stats.success++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'duplicate': {
|
||||||
|
stats.duplicates++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'error': {
|
||||||
|
stats.errors++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
stats,
|
||||||
errorCounter,
|
|
||||||
duplicateCounter,
|
|
||||||
successCounter,
|
|
||||||
totalUploadCounter,
|
|
||||||
remainingUploads,
|
remainingUploads,
|
||||||
hasError,
|
isDismissible,
|
||||||
dismissErrors,
|
|
||||||
isUploading,
|
isUploading,
|
||||||
resetStore,
|
track,
|
||||||
addNewUploadAsset,
|
dismissErrors,
|
||||||
|
reset,
|
||||||
markStarted,
|
markStarted,
|
||||||
|
addItem,
|
||||||
|
updateItem,
|
||||||
|
removeItem,
|
||||||
updateProgress,
|
updateProgress,
|
||||||
updateAsset,
|
subscribe,
|
||||||
removeUploadAsset,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,25 @@ import { t } from 'svelte-i18n';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { getServerErrorMessage, handleError } from './handle-error';
|
import { getServerErrorMessage, handleError } from './handle-error';
|
||||||
|
|
||||||
|
export const addDummyItems = () => {
|
||||||
|
uploadAssetsStore.addItem({ id: 'asset-0', file: { name: 'asset0.jpg', size: 123_456 } as File });
|
||||||
|
uploadAssetsStore.updateItem('asset-0', { state: UploadState.PENDING });
|
||||||
|
uploadAssetsStore.addItem({ id: 'asset-1', file: { name: 'asset1.jpg', size: 123_456 } as File });
|
||||||
|
uploadAssetsStore.updateItem('asset-1', { state: UploadState.STARTED });
|
||||||
|
uploadAssetsStore.updateProgress('asset-1', 75, 100);
|
||||||
|
uploadAssetsStore.addItem({ id: 'asset-2', file: { name: 'asset2.jpg', size: 123_456 } as File });
|
||||||
|
uploadAssetsStore.updateItem('asset-2', { state: UploadState.ERROR, error: new Error('Internal server error') });
|
||||||
|
uploadAssetsStore.addItem({ id: 'asset-3', file: { name: 'asset3.jpg', size: 123_456 } as File });
|
||||||
|
uploadAssetsStore.updateItem('asset-3', { state: UploadState.DUPLICATED, assetId: 'asset-2' });
|
||||||
|
uploadAssetsStore.addItem({ id: 'asset-4', file: { name: 'asset3.jpg', size: 123_456 } as File });
|
||||||
|
uploadAssetsStore.updateItem('asset-4', { state: UploadState.DONE });
|
||||||
|
uploadAssetsStore.track('error');
|
||||||
|
uploadAssetsStore.track('success');
|
||||||
|
uploadAssetsStore.track('duplicate');
|
||||||
|
};
|
||||||
|
|
||||||
|
// addDummyItems();
|
||||||
|
|
||||||
let _extensions: string[];
|
let _extensions: string[];
|
||||||
|
|
||||||
export const uploadExecutionQueue = new ExecutorQueue({ concurrency: 2 });
|
export const uploadExecutionQueue = new ExecutorQueue({ concurrency: 2 });
|
||||||
|
@ -68,7 +87,7 @@ export const fileUploadHandler = async (files: File[], albumId?: string, assetId
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const name = file.name.toLowerCase();
|
const name = file.name.toLowerCase();
|
||||||
if (extensions.some((extension) => name.endsWith(extension))) {
|
if (extensions.some((extension) => name.endsWith(extension))) {
|
||||||
uploadAssetsStore.addNewUploadAsset({ id: getDeviceAssetId(file), file, albumId, assetId });
|
uploadAssetsStore.addItem({ id: getDeviceAssetId(file), file, albumId });
|
||||||
promises.push(uploadExecutionQueue.addTask(() => fileUploader(file, albumId, assetId)));
|
promises.push(uploadExecutionQueue.addTask(() => fileUploader(file, albumId, assetId)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,7 +125,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
|
||||||
let responseData: AssetMediaResponseDto | undefined;
|
let responseData: AssetMediaResponseDto | undefined;
|
||||||
const key = getKey();
|
const key = getKey();
|
||||||
if (crypto?.subtle?.digest && !key) {
|
if (crypto?.subtle?.digest && !key) {
|
||||||
uploadAssetsStore.updateAsset(deviceAssetId, { message: $t('asset_hashing') });
|
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_hashing') });
|
||||||
await tick();
|
await tick();
|
||||||
try {
|
try {
|
||||||
const bytes = await assetFile.arrayBuffer();
|
const bytes = await assetFile.arrayBuffer();
|
||||||
|
@ -127,7 +146,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!responseData) {
|
if (!responseData) {
|
||||||
uploadAssetsStore.updateAsset(deviceAssetId, { message: $t('asset_uploading') });
|
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_uploading') });
|
||||||
if (replaceAssetId) {
|
if (replaceAssetId) {
|
||||||
const response = await uploadRequest<AssetMediaResponseDto>({
|
const response = await uploadRequest<AssetMediaResponseDto>({
|
||||||
url: getBaseUrl() + getAssetOriginalPath(replaceAssetId) + (key ? `?key=${key}` : ''),
|
url: getBaseUrl() + getAssetOriginalPath(replaceAssetId) + (key ? `?key=${key}` : ''),
|
||||||
|
@ -152,30 +171,34 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseData.status === AssetMediaStatus.Duplicate) {
|
if (responseData.status === AssetMediaStatus.Duplicate) {
|
||||||
uploadAssetsStore.duplicateCounter.update((count) => count + 1);
|
uploadAssetsStore.track('duplicate');
|
||||||
} else {
|
} else {
|
||||||
uploadAssetsStore.successCounter.update((c) => c + 1);
|
uploadAssetsStore.track('success');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (albumId) {
|
if (albumId) {
|
||||||
uploadAssetsStore.updateAsset(deviceAssetId, { message: $t('asset_adding_to_album') });
|
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_adding_to_album') });
|
||||||
await addAssetsToAlbum(albumId, [responseData.id], false);
|
await addAssetsToAlbum(albumId, [responseData.id], false);
|
||||||
uploadAssetsStore.updateAsset(deviceAssetId, { message: $t('asset_added_to_album') });
|
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_added_to_album') });
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadAssetsStore.updateAsset(deviceAssetId, {
|
uploadAssetsStore.updateItem(deviceAssetId, {
|
||||||
state: responseData.status === AssetMediaStatus.Duplicate ? UploadState.DUPLICATED : UploadState.DONE,
|
state: responseData.status === AssetMediaStatus.Duplicate ? UploadState.DUPLICATED : UploadState.DONE,
|
||||||
|
assetId: responseData.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
if (responseData.status !== AssetMediaStatus.Duplicate) {
|
||||||
uploadAssetsStore.removeUploadAsset(deviceAssetId);
|
setTimeout(() => {
|
||||||
}, 1000);
|
uploadAssetsStore.removeItem(deviceAssetId);
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
return responseData.id;
|
return responseData.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.unable_to_upload_file'));
|
handleError(error, $t('errors.unable_to_upload_file'));
|
||||||
const reason = getServerErrorMessage(error) || error;
|
const reason = getServerErrorMessage(error) || error;
|
||||||
uploadAssetsStore.updateAsset(deviceAssetId, { state: UploadState.ERROR, error: reason });
|
uploadAssetsStore.track('error');
|
||||||
|
uploadAssetsStore.updateItem(deviceAssetId, { state: UploadState.ERROR, error: reason });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue