mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	refactor(web): upload panel (#12326)
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									0d6bef2c05
								
							
						
					
					
						commit
						f4ec842577
					
				@ -16,13 +16,14 @@
 | 
			
		||||
  export let ariaLabelledby: string | undefined = undefined;
 | 
			
		||||
  export let strokeWidth: number = 0;
 | 
			
		||||
  export let strokeColor: string = 'currentColor';
 | 
			
		||||
  export let spin = false;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svg
 | 
			
		||||
  width={size}
 | 
			
		||||
  height={size}
 | 
			
		||||
  {viewBox}
 | 
			
		||||
  class="{className} {flipped ? '-scale-x-100' : ''}"
 | 
			
		||||
  class="{className} {flipped ? '-scale-x-100' : ''} {spin ? 'animate-spin' : ''}"
 | 
			
		||||
  {role}
 | 
			
		||||
  stroke={strokeColor}
 | 
			
		||||
  stroke-width={strokeWidth}
 | 
			
		||||
 | 
			
		||||
@ -1,21 +1,32 @@
 | 
			
		||||
<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 { UploadState } from '$lib/models/upload-asset';
 | 
			
		||||
  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 Icon from '$lib/components/elements/icon.svelte';
 | 
			
		||||
  import { getByteUnitString } from '$lib/utils/byte-units';
 | 
			
		||||
  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 { fade } from 'svelte/transition';
 | 
			
		||||
 | 
			
		||||
  export let uploadAsset: UploadAsset;
 | 
			
		||||
 | 
			
		||||
  const handleDismiss = (uploadAsset: UploadAsset) => {
 | 
			
		||||
    uploadAssetsStore.removeItem(uploadAsset.id);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleRetry = async (uploadAsset: UploadAsset) => {
 | 
			
		||||
    uploadAssetsStore.removeUploadAsset(uploadAsset.id);
 | 
			
		||||
    uploadAssetsStore.removeItem(uploadAsset.id);
 | 
			
		||||
    await fileUploadHandler([uploadAsset.file], uploadAsset.albumId);
 | 
			
		||||
  };
 | 
			
		||||
</script>
 | 
			
		||||
@ -23,33 +34,55 @@
 | 
			
		||||
<div
 | 
			
		||||
  in:fade={{ duration: 250 }}
 | 
			
		||||
  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="relative">
 | 
			
		||||
      <div in:fade={{ duration: 250 }}>
 | 
			
		||||
        <ImmichLogo noText class="h-[65px] w-[65px] rounded-bl-lg rounded-tl-lg object-cover p-2" />
 | 
			
		||||
  <div class="flex items-center gap-2">
 | 
			
		||||
    <div class="flex items-center justify-center">
 | 
			
		||||
      {#if uploadAsset.state === UploadState.PENDING}
 | 
			
		||||
        <Icon path={mdiCircleOutline} size="24" class="text-immich-primary" title={$t('pending')} />
 | 
			
		||||
      {:else if uploadAsset.state === UploadState.STARTED}
 | 
			
		||||
        <Icon path={mdiLoading} size="24" spin class="text-immich-primary" title={$t('asset_skipped')} />
 | 
			
		||||
      {:else if uploadAsset.state === UploadState.ERROR}
 | 
			
		||||
        <Icon path={mdiAlertCircle} size="24" class="text-immich-error" title={$t('error')} />
 | 
			
		||||
      {:else if uploadAsset.state === UploadState.DUPLICATED}
 | 
			
		||||
        <Icon path={mdiAlertCircle} size="24" class="text-immich-warning" title={$t('asset_skipped')} />
 | 
			
		||||
      {:else if uploadAsset.state === UploadState.DONE}
 | 
			
		||||
        <Icon path={mdiCheckCircle} size="24" class="text-immich-success" title={$t('asset_uploaded')} />
 | 
			
		||||
      {/if}
 | 
			
		||||
    </div>
 | 
			
		||||
      <div class="absolute bottom-0 left-0 h-[25px] w-full rounded-bl-md bg-immich-primary/30">
 | 
			
		||||
        <p
 | 
			
		||||
          class="absolute bottom-1 right-1 stroke-immich-primary object-right-bottom font-semibold uppercase text-white/95 dark:text-gray-100"
 | 
			
		||||
        >
 | 
			
		||||
          .{getFilenameExtension(uploadAsset.file.name)}
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="flex flex-col justify-between p-2 pr-2">
 | 
			
		||||
      <input
 | 
			
		||||
        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}`}
 | 
			
		||||
      />
 | 
			
		||||
    <!-- <span>[{getByteUnitString(uploadAsset.file.size, $locale)}]</span> -->
 | 
			
		||||
    <span class="grow break-all">{uploadAsset.file.name}</span>
 | 
			
		||||
 | 
			
		||||
      <div
 | 
			
		||||
        class="relative mt-[5px] h-[15px] w-full rounded-md bg-gray-300 text-white dark:bg-immich-dark-gray"
 | 
			
		||||
        class:dark:text-black={uploadAsset.state === UploadState.STARTED}
 | 
			
		||||
    {#if uploadAsset.state === UploadState.DUPLICATED && uploadAsset.assetId}
 | 
			
		||||
      <div class="flex items-center justify-between gap-1">
 | 
			
		||||
        <a
 | 
			
		||||
          href="{AppRoute.PHOTOS}/{uploadAsset.assetId}"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          rel="noopener noreferrer"
 | 
			
		||||
          class=""
 | 
			
		||||
          aria-hidden="true"
 | 
			
		||||
          tabindex={-1}
 | 
			
		||||
        >
 | 
			
		||||
          <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>
 | 
			
		||||
      </div>
 | 
			
		||||
    {/if}
 | 
			
		||||
  </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}
 | 
			
		||||
@ -58,51 +91,12 @@
 | 
			
		||||
          {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>
 | 
			
		||||
        </button>
 | 
			
		||||
    </div>
 | 
			
		||||
  {/if}
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  {#if uploadAsset.state === UploadState.ERROR}
 | 
			
		||||
    <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}
 | 
			
		||||
      </p>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@ -15,8 +15,7 @@
 | 
			
		||||
  let showOptions = false;
 | 
			
		||||
  let concurrency = uploadExecutionQueue.concurrency;
 | 
			
		||||
 | 
			
		||||
  let { isUploading, hasError, remainingUploads, errorCounter, duplicateCounter, successCounter, totalUploadCounter } =
 | 
			
		||||
    uploadAssetsStore;
 | 
			
		||||
  let { stats, isDismissible, isUploading, remainingUploads } = uploadAssetsStore;
 | 
			
		||||
 | 
			
		||||
  const autoHide = () => {
 | 
			
		||||
    if (!$isUploading && showDetail) {
 | 
			
		||||
@ -33,29 +32,29 @@
 | 
			
		||||
  }
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if $hasError || $isUploading}
 | 
			
		||||
{#if $isUploading}
 | 
			
		||||
  <div
 | 
			
		||||
    in:fade={{ duration: 250 }}
 | 
			
		||||
    out:fade={{ duration: 250 }}
 | 
			
		||||
    on:outroend={() => {
 | 
			
		||||
      if ($errorCounter > 0) {
 | 
			
		||||
      if ($stats.errors > 0) {
 | 
			
		||||
        notificationController.show({
 | 
			
		||||
          message: $t('upload_errors', { values: { count: $errorCounter } }),
 | 
			
		||||
          message: $t('upload_errors', { values: { count: $stats.errors } }),
 | 
			
		||||
          type: NotificationType.Warning,
 | 
			
		||||
        });
 | 
			
		||||
      } else if ($successCounter > 0) {
 | 
			
		||||
      } else if ($stats.success > 0) {
 | 
			
		||||
        notificationController.show({
 | 
			
		||||
          message: $t('upload_success'),
 | 
			
		||||
          type: NotificationType.Info,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      if ($duplicateCounter > 0) {
 | 
			
		||||
      if ($stats.duplicates > 0) {
 | 
			
		||||
        notificationController.show({
 | 
			
		||||
          message: $t('upload_skipped_duplicates', { values: { count: $duplicateCounter } }),
 | 
			
		||||
          message: $t('upload_skipped_duplicates', { values: { count: $stats.duplicates } }),
 | 
			
		||||
          type: NotificationType.Warning,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      uploadAssetsStore.resetStore();
 | 
			
		||||
      uploadAssetsStore.reset();
 | 
			
		||||
    }}
 | 
			
		||||
    class="fixed bottom-6 right-6 z-[10000]"
 | 
			
		||||
  >
 | 
			
		||||
@ -70,20 +69,20 @@
 | 
			
		||||
              {$t('upload_progress', {
 | 
			
		||||
                values: {
 | 
			
		||||
                  remaining: $remainingUploads,
 | 
			
		||||
                  processed: $successCounter + $errorCounter,
 | 
			
		||||
                  total: $totalUploadCounter,
 | 
			
		||||
                  processed: $stats.total - $remainingUploads,
 | 
			
		||||
                  total: $stats.total,
 | 
			
		||||
                },
 | 
			
		||||
              })}
 | 
			
		||||
            </p>
 | 
			
		||||
            <p class="immich-form-label text-xs">
 | 
			
		||||
              {$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')}
 | 
			
		||||
              <span class="text-immich-error">{$errorCounter.toLocaleString($locale)}</span>
 | 
			
		||||
              <span class="text-immich-error">{$stats.errors.toLocaleString($locale)}</span>
 | 
			
		||||
              -
 | 
			
		||||
              {$t('upload_status_duplicates')}
 | 
			
		||||
              <span class="text-immich-warning">{$duplicateCounter.toLocaleString($locale)}</span>
 | 
			
		||||
              <span class="text-immich-warning">{$stats.duplicates.toLocaleString($locale)}</span>
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="flex flex-col items-end">
 | 
			
		||||
@ -103,7 +102,7 @@
 | 
			
		||||
                on:click={() => (showDetail = false)}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
            {#if $hasError}
 | 
			
		||||
            {#if $isDismissible}
 | 
			
		||||
              <CircleIconButton
 | 
			
		||||
                title={$t('dismiss_all_errors')}
 | 
			
		||||
                icon={mdiCancel}
 | 
			
		||||
@ -115,7 +114,7 @@
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        {#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">
 | 
			
		||||
              <label class="immich-form-label" for="upload-concurrency">{$t('upload_concurrency')}</label>
 | 
			
		||||
            </div>
 | 
			
		||||
@ -133,7 +132,7 @@
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        {/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)}
 | 
			
		||||
            <UploadAssetPreview {uploadAsset} />
 | 
			
		||||
          {/each}
 | 
			
		||||
@ -149,14 +148,14 @@
 | 
			
		||||
        >
 | 
			
		||||
          {$remainingUploads.toLocaleString($locale)}
 | 
			
		||||
        </button>
 | 
			
		||||
        {#if $hasError}
 | 
			
		||||
        {#if $stats.errors > 0}
 | 
			
		||||
          <button
 | 
			
		||||
            type="button"
 | 
			
		||||
            in:scale={{ duration: 250, easing: quartInOut }}
 | 
			
		||||
            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"
 | 
			
		||||
          >
 | 
			
		||||
            {$errorCounter.toLocaleString($locale)}
 | 
			
		||||
            {$stats.errors.toLocaleString($locale)}
 | 
			
		||||
          </button>
 | 
			
		||||
        {/if}
 | 
			
		||||
        <button
 | 
			
		||||
 | 
			
		||||
@ -9,8 +9,8 @@ export enum UploadState {
 | 
			
		||||
export type UploadAsset = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  file: File;
 | 
			
		||||
  albumId?: string;
 | 
			
		||||
  assetId?: string;
 | 
			
		||||
  albumId?: string;
 | 
			
		||||
  progress?: number;
 | 
			
		||||
  state?: UploadState;
 | 
			
		||||
  startDate?: number;
 | 
			
		||||
 | 
			
		||||
@ -3,32 +3,36 @@ import { UploadState, type UploadAsset } from '../models/upload-asset';
 | 
			
		||||
 | 
			
		||||
function createUploadStore() {
 | 
			
		||||
  const uploadAssets = writable<Array<UploadAsset>>([]);
 | 
			
		||||
 | 
			
		||||
  const duplicateCounter = writable(0);
 | 
			
		||||
  const successCounter = writable(0);
 | 
			
		||||
  const totalUploadCounter = writable(0);
 | 
			
		||||
  const stats = writable<{ errors: number; duplicates: number; success: number; total: number }>({
 | 
			
		||||
    errors: 0,
 | 
			
		||||
    duplicates: 0,
 | 
			
		||||
    success: 0,
 | 
			
		||||
    total: 0,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const { subscribe } = uploadAssets;
 | 
			
		||||
 | 
			
		||||
  const isUploading = derived(uploadAssets, ($uploadAssets) => {
 | 
			
		||||
    return $uploadAssets.length > 0;
 | 
			
		||||
  });
 | 
			
		||||
  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 isUploading = derived(uploadAssets, (items) => items.length > 0);
 | 
			
		||||
  const isDismissible = derived(uploadAssets, (items) =>
 | 
			
		||||
    items.some((item) => item.state === UploadState.ERROR || item.state === UploadState.DUPLICATED),
 | 
			
		||||
  );
 | 
			
		||||
  const remainingUploads = derived(
 | 
			
		||||
    uploadAssets,
 | 
			
		||||
    (values) => values.filter((a) => a.state === UploadState.PENDING || a.state === UploadState.STARTED).length,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const addNewUploadAsset = (newAsset: UploadAsset) => {
 | 
			
		||||
  const addItem = (newAsset: UploadAsset) => {
 | 
			
		||||
    uploadAssets.update(($assets) => {
 | 
			
		||||
      const duplicate = $assets.find((asset) => asset.id === newAsset.id);
 | 
			
		||||
      if (duplicate) {
 | 
			
		||||
        return $assets.map((asset) => (asset.id === newAsset.id ? newAsset : asset));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      totalUploadCounter.update((c) => c + 1);
 | 
			
		||||
      stats.update((stats) => {
 | 
			
		||||
        stats.total++;
 | 
			
		||||
        return stats;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      $assets.push({
 | 
			
		||||
        ...newAsset,
 | 
			
		||||
        speed: 0,
 | 
			
		||||
@ -36,6 +40,7 @@ function createUploadStore() {
 | 
			
		||||
        progress: 0,
 | 
			
		||||
        eta: 0,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return $assets;
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
@ -53,7 +58,7 @@ function createUploadStore() {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const markStarted = (id: string) => {
 | 
			
		||||
    updateAsset(id, {
 | 
			
		||||
    updateItem(id, {
 | 
			
		||||
      state: UploadState.STARTED,
 | 
			
		||||
      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 }));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const removeUploadAsset = (id: string) => {
 | 
			
		||||
  const removeItem = (id: string) => {
 | 
			
		||||
    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([]);
 | 
			
		||||
    duplicateCounter.set(0);
 | 
			
		||||
    successCounter.set(0);
 | 
			
		||||
    totalUploadCounter.set(0);
 | 
			
		||||
    stats.set({ errors: 0, duplicates: 0, success: 0, total: 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 {
 | 
			
		||||
    subscribe,
 | 
			
		||||
    errorCounter,
 | 
			
		||||
    duplicateCounter,
 | 
			
		||||
    successCounter,
 | 
			
		||||
    totalUploadCounter,
 | 
			
		||||
    stats,
 | 
			
		||||
    remainingUploads,
 | 
			
		||||
    hasError,
 | 
			
		||||
    dismissErrors,
 | 
			
		||||
    isDismissible,
 | 
			
		||||
    isUploading,
 | 
			
		||||
    resetStore,
 | 
			
		||||
    addNewUploadAsset,
 | 
			
		||||
    track,
 | 
			
		||||
    dismissErrors,
 | 
			
		||||
    reset,
 | 
			
		||||
    markStarted,
 | 
			
		||||
    addItem,
 | 
			
		||||
    updateItem,
 | 
			
		||||
    removeItem,
 | 
			
		||||
    updateProgress,
 | 
			
		||||
    updateAsset,
 | 
			
		||||
    removeUploadAsset,
 | 
			
		||||
    subscribe,
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,25 @@ import { t } from 'svelte-i18n';
 | 
			
		||||
import { get } from 'svelte/store';
 | 
			
		||||
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[];
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
    const name = file.name.toLowerCase();
 | 
			
		||||
    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)));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@ -106,7 +125,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
 | 
			
		||||
    let responseData: AssetMediaResponseDto | undefined;
 | 
			
		||||
    const key = getKey();
 | 
			
		||||
    if (crypto?.subtle?.digest && !key) {
 | 
			
		||||
      uploadAssetsStore.updateAsset(deviceAssetId, { message: $t('asset_hashing') });
 | 
			
		||||
      uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_hashing') });
 | 
			
		||||
      await tick();
 | 
			
		||||
      try {
 | 
			
		||||
        const bytes = await assetFile.arrayBuffer();
 | 
			
		||||
@ -127,7 +146,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!responseData) {
 | 
			
		||||
      uploadAssetsStore.updateAsset(deviceAssetId, { message: $t('asset_uploading') });
 | 
			
		||||
      uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_uploading') });
 | 
			
		||||
      if (replaceAssetId) {
 | 
			
		||||
        const response = await uploadRequest<AssetMediaResponseDto>({
 | 
			
		||||
          url: getBaseUrl() + getAssetOriginalPath(replaceAssetId) + (key ? `?key=${key}` : ''),
 | 
			
		||||
@ -152,30 +171,34 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (responseData.status === AssetMediaStatus.Duplicate) {
 | 
			
		||||
      uploadAssetsStore.duplicateCounter.update((count) => count + 1);
 | 
			
		||||
      uploadAssetsStore.track('duplicate');
 | 
			
		||||
    } else {
 | 
			
		||||
      uploadAssetsStore.successCounter.update((c) => c + 1);
 | 
			
		||||
      uploadAssetsStore.track('success');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
      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,
 | 
			
		||||
      assetId: responseData.id,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (responseData.status !== AssetMediaStatus.Duplicate) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
      uploadAssetsStore.removeUploadAsset(deviceAssetId);
 | 
			
		||||
        uploadAssetsStore.removeItem(deviceAssetId);
 | 
			
		||||
      }, 1000);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return responseData.id;
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    handleError(error, $t('errors.unable_to_upload_file'));
 | 
			
		||||
    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;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user