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 () => {
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) => {

View File

@ -1,6 +1,15 @@
<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 { 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>
{#if $isDownloading}
@ -10,16 +19,27 @@
>
<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">
{#each Object.keys($downloadAssets) as fileName}
<div class="mb-2" transition:slide>
<p class="font-medium text-xs truncate">{fileName}</p>
<div class="flex flex-row-reverse place-items-center gap-5">
<p>
<span class="text-immich-primary font-medium">{$downloadAssets[fileName]}</span>/100
</p>
<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: ${$downloadAssets[fileName]}%`} />
{#each Object.keys($downloadAssets) as downloadKey (downloadKey)}
{@const download = $downloadAssets[downloadKey]}
<div class="mb-2 flex place-items-center" transition:slide>
<div class="w-full pr-10">
<div class="font-medium text-xs flex gap-2 place-items-center justify-between">
<p class="truncate">{downloadKey}</p>
{#if download.total}
<p class="whitespace-nowrap">{asByteUnitString(download.total, $locale)}</p>
{/if}
</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>
{/each}

View File

@ -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);
};
</script>

View File

@ -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[] = []) => {

View File

@ -1,6 +1,13 @@
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) => {
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<DownloadProgress> | 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<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 { 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<AssetApiGetDownloadInfoRequest, 'key'>,
onDone?: () => void,
key?: string,
) => {
let downloadInfo: DownloadResponseDto | null = null;
@ -63,60 +62,72 @@ export const downloadArchive = async (
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);
}
}
};

View File

@ -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;