mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00: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
|
|
@ -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,86 +34,69 @@
|
|||
<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>
|
||||
<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 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="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.STARTED}
|
||||
<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>
|
||||
{: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"
|
||||
{#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}
|
||||
>
|
||||
<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>
|
||||
</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}
|
||||
{uploadAsset.message}
|
||||
{:else}
|
||||
{uploadAsset.progress}% - {getByteUnitString(uploadAsset.speed || 0, $locale)}/s - {uploadAsset.eta}s
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue