1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

feat(web): show download size (#3270)

* feat(web): show download size

* chore: never over 100%

* chore: use percentage

* fix: unselect assets before download finishes
This commit is contained in:
Jason Rasmussen 2023-07-14 21:25:13 -04:00 committed by GitHub
parent 81e07fda08
commit 382341f550
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 99 additions and 42 deletions

View File

@ -235,7 +235,7 @@
}; };
const downloadAlbum = async () => { 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) => { const showAlbumOptionsMenu = ({ x, y }: MouseEvent) => {

View File

@ -1,6 +1,15 @@
<script lang="ts"> <script lang="ts">
import { downloadAssets, isDownloading } from '$lib/stores/download'; import { DownloadProgress, downloadAssets, downloadManager, isDownloading } from '$lib/stores/download';
import { locale } from '$lib/stores/preferences.store';
import Close from 'svelte-material-icons/Close.svelte';
import { fly, slide } from 'svelte/transition'; import { fly, slide } from 'svelte/transition';
import { asByteUnitString } from '../../utils/byte-units';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
const abort = (downloadKey: string, download: DownloadProgress) => {
download.abort?.abort();
downloadManager.clear(downloadKey);
};
</script> </script>
{#if $isDownloading} {#if $isDownloading}
@ -10,16 +19,27 @@
> >
<p class="text-gray-500 text-xs mb-2">DOWNLOADING</p> <p class="text-gray-500 text-xs mb-2">DOWNLOADING</p>
<div class="max-h-[200px] my-2 overflow-y-auto mb-2 flex flex-col text-sm"> <div class="max-h-[200px] my-2 overflow-y-auto mb-2 flex flex-col text-sm">
{#each Object.keys($downloadAssets) as fileName} {#each Object.keys($downloadAssets) as downloadKey (downloadKey)}
<div class="mb-2" transition:slide> {@const download = $downloadAssets[downloadKey]}
<p class="font-medium text-xs truncate">{fileName}</p> <div class="mb-2 flex place-items-center" transition:slide>
<div class="flex flex-row-reverse place-items-center gap-5"> <div class="w-full pr-10">
<p> <div class="font-medium text-xs flex gap-2 place-items-center justify-between">
<span class="text-immich-primary font-medium">{$downloadAssets[fileName]}</span>/100 <p class="truncate">{downloadKey}</p>
</p> {#if download.total}
<div class="w-full bg-gray-200 rounded-full h-[7px] dark:bg-gray-700"> <p class="whitespace-nowrap">{asByteUnitString(download.total, $locale)}</p>
<div class="bg-immich-primary h-[7px] rounded-full" style={`width: ${$downloadAssets[fileName]}%`} /> {/if}
</div> </div>
<div class="flex place-items-center gap-2">
<div class="w-full bg-gray-200 rounded-full h-[7px] dark:bg-gray-700">
<div class="bg-immich-primary h-[7px] rounded-full" style={`width: ${download.percentage}%`} />
</div>
<p class="whitespace-nowrap min-w-[4em] text-right">
<span class="text-immich-primary">{download.percentage}%</span>
</p>
</div>
</div>
<div class="absolute right-2">
<CircleIconButton on:click={() => abort(downloadKey, download)} size="20" logo={Close} forceDark />
</div> </div>
</div> </div>
{/each} {/each}

View File

@ -14,12 +14,13 @@
const handleDownloadFiles = async () => { const handleDownloadFiles = async () => {
const assets = Array.from(getAssets()); const assets = Array.from(getAssets());
if (assets.length === 1) { if (assets.length === 1) {
await downloadFile(assets[0], sharedLinkKey);
clearSelect(); clearSelect();
await downloadFile(assets[0], sharedLinkKey);
return; return;
} }
await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) }, clearSelect, sharedLinkKey); clearSelect();
await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) }, sharedLinkKey);
}; };
</script> </script>

View File

@ -35,12 +35,7 @@
}); });
const downloadAssets = async () => { const downloadAssets = async () => {
await downloadArchive( await downloadArchive(`immich-shared.zip`, { assetIds: assets.map((asset) => asset.id) }, sharedLink.key);
`immich-shared.zip`,
{ assetIds: assets.map((asset) => asset.id) },
undefined,
sharedLink.key,
);
}; };
const handleUploadAssets = async (files: File[] = []) => { const handleUploadAssets = async (files: File[] = []) => {

View File

@ -1,6 +1,13 @@
import { derived, writable } from 'svelte/store'; import { derived, writable } from 'svelte/store';
export const downloadAssets = writable<Record<string, number>>({}); export interface DownloadProgress {
progress: number;
total: number;
percentage: number;
abort: AbortController | null;
}
export const downloadAssets = writable<Record<string, DownloadProgress>>({});
export const isDownloading = derived(downloadAssets, ($downloadAssets) => { export const isDownloading = derived(downloadAssets, ($downloadAssets) => {
if (Object.keys($downloadAssets).length == 0) { if (Object.keys($downloadAssets).length == 0) {
@ -10,17 +17,35 @@ export const isDownloading = derived(downloadAssets, ($downloadAssets) => {
return true; return true;
}); });
const update = (key: string, value: number | null) => { const update = (key: string, value: Partial<DownloadProgress> | null) => {
downloadAssets.update((state) => { downloadAssets.update((state) => {
const newState = { ...state }; const newState = { ...state };
if (value === null) { if (value === null) {
delete newState[key]; delete newState[key];
} else { return newState;
newState[key] = value;
} }
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; return newState;
}); });
}; };
export const clearDownload = (key: string) => update(key, null); export const downloadManager = {
export const updateDownload = (key: string, value: number) => update(key, value); 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<DownloadProgress> = { progress };
if (total !== undefined) {
download.total = total;
}
update(key, download);
},
};

View File

@ -1,5 +1,5 @@
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; 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 { AddAssetsResponseDto, api, AssetApiGetDownloadInfoRequest, AssetResponseDto, DownloadResponseDto } from '@api';
import { handleError } from './handle-error'; import { handleError } from './handle-error';
@ -37,7 +37,6 @@ const downloadBlob = (data: Blob, filename: string) => {
export const downloadArchive = async ( export const downloadArchive = async (
fileName: string, fileName: string,
options: Omit<AssetApiGetDownloadInfoRequest, 'key'>, options: Omit<AssetApiGetDownloadInfoRequest, 'key'>,
onDone?: () => void,
key?: string, key?: string,
) => { ) => {
let downloadInfo: DownloadResponseDto | null = null; let downloadInfo: DownloadResponseDto | null = null;
@ -58,65 +57,77 @@ export const downloadArchive = async (
const suffix = downloadInfo.archives.length === 1 ? '' : `+${i + 1}`; const suffix = downloadInfo.archives.length === 1 ? '' : `+${i + 1}`;
const archiveName = fileName.replace('.zip', `${suffix}.zip`); const archiveName = fileName.replace('.zip', `${suffix}.zip`);
let downloadKey = `${archiveName}`; let downloadKey = `${archiveName} `;
if (downloadInfo.archives.length > 1) { if (downloadInfo.archives.length > 1) {
downloadKey = `${archiveName} (${i + 1}/${downloadInfo.archives.length})`; downloadKey = `${archiveName} (${i + 1}/${downloadInfo.archives.length})`;
} }
updateDownload(downloadKey, 0); const abort = new AbortController();
downloadManager.add(downloadKey, archive.size, abort);
try { try {
const { data } = await api.assetApi.downloadArchive( const { data } = await api.assetApi.downloadArchive(
{ assetIdsDto: { assetIds: archive.assetIds }, key }, { assetIdsDto: { assetIds: archive.assetIds }, key },
{ {
responseType: 'blob', 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); downloadBlob(data, archiveName);
} catch (e) { } catch (e) {
handleError(e, 'Unable to download files'); handleError(e, 'Unable to download files');
clearDownload(downloadKey); downloadManager.clear(downloadKey);
return; return;
} finally { } finally {
setTimeout(() => clearDownload(downloadKey), 3_000); setTimeout(() => downloadManager.clear(downloadKey), 5_000);
} }
} }
onDone?.();
}; };
export const downloadFile = async (asset: AssetResponseDto, key?: string) => { 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) { if (asset.livePhotoVideoId) {
assets.push({ assets.push({
filename: `${asset.originalFileName}.mov`, filename: `${asset.originalFileName}.mov`,
id: asset.livePhotoVideoId, id: asset.livePhotoVideoId,
size: 0,
}); });
} }
for (const asset of assets) { for (const { filename, id, size } of assets) {
const downloadKey = filename;
try { try {
updateDownload(asset.filename, 0); const abort = new AbortController();
downloadManager.add(downloadKey, size, abort);
const { data } = await api.assetApi.downloadFile( const { data } = await api.assetApi.downloadFile(
{ id: asset.id, key }, { id, key },
{ {
responseType: 'blob', responseType: 'blob',
onDownloadProgress: (event: ProgressEvent) => { onDownloadProgress: (event: ProgressEvent) => {
if (event.lengthComputable) { 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) { } catch (e) {
handleError(e, `Error downloading ${asset.filename}`); handleError(e, `Error downloading ${filename}`);
downloadManager.clear(downloadKey);
} finally { } finally {
setTimeout(() => clearDownload(asset.filename), 3_000); setTimeout(() => downloadManager.clear(downloadKey), 5_000);
} }
} }
}; };

View File

@ -1,7 +1,12 @@
import type { ApiError } from '@api'; import type { ApiError } from '@api';
import { CanceledError } from 'axios';
import { notificationController, NotificationType } from '../components/shared-components/notification/notification'; import { notificationController, NotificationType } from '../components/shared-components/notification/notification';
export async function handleError(error: unknown, message: string) { export async function handleError(error: unknown, message: string) {
if (error instanceof CanceledError) {
return;
}
console.error(`[handleError]: ${message}`, error); console.error(`[handleError]: ${message}`, error);
let data = (error as ApiError)?.response?.data; let data = (error as ApiError)?.response?.data;