From 382341f5508d8dfcf48e3382341bfb4a5b46838c Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 14 Jul 2023 21:25:13 -0400 Subject: [PATCH] feat(web): show download size (#3270) * feat(web): show download size * chore: never over 100% * chore: use percentage * fix: unselect assets before download finishes --- .../components/album-page/album-viewer.svelte | 2 +- .../asset-viewer/download-panel.svelte | 40 ++++++++++++----- .../actions/download-action.svelte | 5 ++- .../individual-shared-viewer.svelte | 7 +-- web/src/lib/stores/download.ts | 37 ++++++++++++--- web/src/lib/utils/asset-utils.ts | 45 ++++++++++++------- web/src/lib/utils/handle-error.ts | 5 +++ 7 files changed, 99 insertions(+), 42 deletions(-) diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index c56a579b9d..3ec095c5a5 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -235,7 +235,7 @@ }; const downloadAlbum = async () => { - await downloadArchive(`${album.albumName}.zip`, { albumId: album.id }, undefined, sharedLink?.key); + await downloadArchive(`${album.albumName}.zip`, { albumId: album.id }, sharedLink?.key); }; const showAlbumOptionsMenu = ({ x, y }: MouseEvent) => { diff --git a/web/src/lib/components/asset-viewer/download-panel.svelte b/web/src/lib/components/asset-viewer/download-panel.svelte index 3f0c872d2e..d4d56f932d 100644 --- a/web/src/lib/components/asset-viewer/download-panel.svelte +++ b/web/src/lib/components/asset-viewer/download-panel.svelte @@ -1,6 +1,15 @@ {#if $isDownloading} @@ -10,16 +19,27 @@ >

DOWNLOADING

- {#each Object.keys($downloadAssets) as fileName} -
-

■ {fileName}

-
-

- {$downloadAssets[fileName]}/100 -

-
-
+ {#each Object.keys($downloadAssets) as downloadKey (downloadKey)} + {@const download = $downloadAssets[downloadKey]} +
+
+
+

■ {downloadKey}

+ {#if download.total} +

{asByteUnitString(download.total, $locale)}

+ {/if}
+
+
+
+
+

+ {download.percentage}% +

+
+
+
+ abort(downloadKey, download)} size="20" logo={Close} forceDark />
{/each} diff --git a/web/src/lib/components/photos-page/actions/download-action.svelte b/web/src/lib/components/photos-page/actions/download-action.svelte index 43b3a9ae42..e1fb24c085 100644 --- a/web/src/lib/components/photos-page/actions/download-action.svelte +++ b/web/src/lib/components/photos-page/actions/download-action.svelte @@ -14,12 +14,13 @@ const handleDownloadFiles = async () => { const assets = Array.from(getAssets()); if (assets.length === 1) { - await downloadFile(assets[0], sharedLinkKey); clearSelect(); + await downloadFile(assets[0], sharedLinkKey); return; } - await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) }, clearSelect, sharedLinkKey); + clearSelect(); + await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) }, sharedLinkKey); }; diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte index aa16f1b458..0f883a4a8f 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte @@ -35,12 +35,7 @@ }); const downloadAssets = async () => { - await downloadArchive( - `immich-shared.zip`, - { assetIds: assets.map((asset) => asset.id) }, - undefined, - sharedLink.key, - ); + await downloadArchive(`immich-shared.zip`, { assetIds: assets.map((asset) => asset.id) }, sharedLink.key); }; const handleUploadAssets = async (files: File[] = []) => { diff --git a/web/src/lib/stores/download.ts b/web/src/lib/stores/download.ts index a7a9f81c07..7dd13b18cf 100644 --- a/web/src/lib/stores/download.ts +++ b/web/src/lib/stores/download.ts @@ -1,6 +1,13 @@ import { derived, writable } from 'svelte/store'; -export const downloadAssets = writable>({}); +export interface DownloadProgress { + progress: number; + total: number; + percentage: number; + abort: AbortController | null; +} + +export const downloadAssets = writable>({}); export const isDownloading = derived(downloadAssets, ($downloadAssets) => { if (Object.keys($downloadAssets).length == 0) { @@ -10,17 +17,35 @@ export const isDownloading = derived(downloadAssets, ($downloadAssets) => { return true; }); -const update = (key: string, value: number | null) => { +const update = (key: string, value: Partial | null) => { downloadAssets.update((state) => { const newState = { ...state }; + if (value === null) { delete newState[key]; - } else { - newState[key] = value; + return newState; } + + if (!newState[key]) { + newState[key] = { progress: 0, total: 0, percentage: 0, abort: null }; + } + + const item = newState[key]; + Object.assign(item, value); + item.percentage = Math.min(Math.floor((item.progress / item.total) * 100), 100); + return newState; }); }; -export const clearDownload = (key: string) => update(key, null); -export const updateDownload = (key: string, value: number) => update(key, value); +export const downloadManager = { + add: (key: string, total: number, abort?: AbortController) => update(key, { total, abort }), + clear: (key: string) => update(key, null), + update: (key: string, progress: number, total?: number) => { + const download: Partial = { progress }; + if (total !== undefined) { + download.total = total; + } + update(key, download); + }, +}; diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index d420d9f57e..55169acf1f 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -1,5 +1,5 @@ import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; -import { clearDownload, updateDownload } from '$lib/stores/download'; +import { downloadManager } from '$lib/stores/download'; import { AddAssetsResponseDto, api, AssetApiGetDownloadInfoRequest, AssetResponseDto, DownloadResponseDto } from '@api'; import { handleError } from './handle-error'; @@ -37,7 +37,6 @@ const downloadBlob = (data: Blob, filename: string) => { export const downloadArchive = async ( fileName: string, options: Omit, - onDone?: () => void, key?: string, ) => { let downloadInfo: DownloadResponseDto | null = null; @@ -58,65 +57,77 @@ export const downloadArchive = async ( const suffix = downloadInfo.archives.length === 1 ? '' : `+${i + 1}`; const archiveName = fileName.replace('.zip', `${suffix}.zip`); - let downloadKey = `${archiveName}`; + let downloadKey = `${archiveName} `; if (downloadInfo.archives.length > 1) { downloadKey = `${archiveName} (${i + 1}/${downloadInfo.archives.length})`; } - updateDownload(downloadKey, 0); + const abort = new AbortController(); + downloadManager.add(downloadKey, archive.size, abort); try { const { data } = await api.assetApi.downloadArchive( { assetIdsDto: { assetIds: archive.assetIds }, key }, { responseType: 'blob', - onDownloadProgress: (event) => updateDownload(downloadKey, Math.floor((event.loaded / archive.size) * 100)), + signal: abort.signal, + onDownloadProgress: (event) => downloadManager.update(downloadKey, event.loaded), }, ); downloadBlob(data, archiveName); } catch (e) { handleError(e, 'Unable to download files'); - clearDownload(downloadKey); + downloadManager.clear(downloadKey); return; } finally { - setTimeout(() => clearDownload(downloadKey), 3_000); + setTimeout(() => downloadManager.clear(downloadKey), 5_000); } } - - onDone?.(); }; export const downloadFile = async (asset: AssetResponseDto, key?: string) => { - const assets = [{ filename: `${asset.originalFileName}.${getFilenameExtension(asset.originalPath)}`, id: asset.id }]; + const assets = [ + { + filename: `${asset.originalFileName}.${getFilenameExtension(asset.originalPath)}`, + id: asset.id, + size: asset.exifInfo?.fileSizeInByte || 0, + }, + ]; if (asset.livePhotoVideoId) { assets.push({ filename: `${asset.originalFileName}.mov`, id: asset.livePhotoVideoId, + size: 0, }); } - for (const asset of assets) { + for (const { filename, id, size } of assets) { + const downloadKey = filename; + try { - updateDownload(asset.filename, 0); + const abort = new AbortController(); + downloadManager.add(downloadKey, size, abort); const { data } = await api.assetApi.downloadFile( - { id: asset.id, key }, + { id, key }, { responseType: 'blob', onDownloadProgress: (event: ProgressEvent) => { if (event.lengthComputable) { - updateDownload(asset.filename, Math.floor((event.loaded / event.total) * 100)); + downloadManager.update(downloadKey, event.loaded, event.total); } }, + signal: abort.signal, }, ); - downloadBlob(data, asset.filename); + downloadBlob(data, filename); } catch (e) { - handleError(e, `Error downloading ${asset.filename}`); + handleError(e, `Error downloading ${filename}`); + downloadManager.clear(downloadKey); } finally { - setTimeout(() => clearDownload(asset.filename), 3_000); + setTimeout(() => downloadManager.clear(downloadKey), 5_000); } } }; diff --git a/web/src/lib/utils/handle-error.ts b/web/src/lib/utils/handle-error.ts index 3c9c0e1c93..32a73f03d0 100644 --- a/web/src/lib/utils/handle-error.ts +++ b/web/src/lib/utils/handle-error.ts @@ -1,7 +1,12 @@ import type { ApiError } from '@api'; +import { CanceledError } from 'axios'; import { notificationController, NotificationType } from '../components/shared-components/notification/notification'; export async function handleError(error: unknown, message: string) { + if (error instanceof CanceledError) { + return; + } + console.error(`[handleError]: ${message}`, error); let data = (error as ApiError)?.response?.data;