import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; import { downloadManager } from '$lib/stores/download'; import { api, type BulkIdResponseDto, type AssetResponseDto, type DownloadResponseDto, type DownloadInfoDto, AssetTypeEnum, type UserResponseDto, } from '@api'; import { handleError } from './handle-error'; export const addAssetsToAlbum = async (albumId: string, assetIds: Array): Promise => api.albumApi .addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetIds }, key: api.getKey(), }) .then(({ data: results }) => { const count = results.filter(({ success }) => success).length; notificationController.show({ type: NotificationType.Info, message: `Added ${count} asset${count === 1 ? '' : 's'}`, }); return results; }); export const downloadBlob = (data: Blob, filename: string) => { const url = URL.createObjectURL(data); const anchor = document.createElement('a'); anchor.href = url; anchor.download = filename; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); URL.revokeObjectURL(url); }; export const downloadArchive = async (fileName: string, options: DownloadInfoDto) => { let downloadInfo: DownloadResponseDto | null = null; try { const { data } = await api.assetApi.getDownloadInfo({ downloadInfoDto: options, key: api.getKey() }); downloadInfo = data; } catch (error) { handleError(error, 'Unable to download files'); return; } // TODO: prompt for big download // const total = downloadInfo.totalSize; for (let i = 0; i < downloadInfo.archives.length; i++) { const archive = downloadInfo.archives[i]; const suffix = downloadInfo.archives.length === 1 ? '' : `+${i + 1}`; const archiveName = fileName.replace('.zip', `${suffix}.zip`); let downloadKey = `${archiveName} `; if (downloadInfo.archives.length > 1) { downloadKey = `${archiveName} (${i + 1}/${downloadInfo.archives.length})`; } const abort = new AbortController(); downloadManager.add(downloadKey, archive.size, abort); try { const { data } = await api.assetApi.downloadArchive( { assetIdsDto: { assetIds: archive.assetIds }, key: api.getKey() }, { responseType: 'blob', signal: abort.signal, onDownloadProgress: (event) => downloadManager.update(downloadKey, event.loaded), }, ); downloadBlob(data, archiveName); } catch (e) { handleError(e, 'Unable to download files'); downloadManager.clear(downloadKey); return; } finally { setTimeout(() => downloadManager.clear(downloadKey), 5_000); } } }; export const downloadFile = async (asset: AssetResponseDto) => { if (asset.isOffline) { notificationController.show({ type: NotificationType.Info, message: `Asset ${asset.originalFileName} is offline`, }); return; } 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 { filename, id, size } of assets) { const downloadKey = filename; try { const abort = new AbortController(); downloadManager.add(downloadKey, size, abort); const { data } = await api.assetApi.downloadFile( { id, key: api.getKey() }, { responseType: 'blob', onDownloadProgress: (event: ProgressEvent) => { if (event.lengthComputable) { downloadManager.update(downloadKey, event.loaded, event.total); } }, signal: abort.signal, }, ); notificationController.show({ type: NotificationType.Info, message: `Downloading asset ${asset.originalFileName}`, }); downloadBlob(data, filename); } catch (e) { handleError(e, `Error downloading ${filename}`); downloadManager.clear(downloadKey); } finally { setTimeout(() => downloadManager.clear(downloadKey), 5_000); } } }; /** * Returns the lowercase filename extension without a dot (.) and * an empty string when not found. */ export function getFilenameExtension(filename: string): string { const lastIndex = Math.max(0, filename.lastIndexOf('.')); const startIndex = (lastIndex || Infinity) + 1; return filename.slice(startIndex).toLowerCase(); } /** * Returns the filename of an asset including file extension */ export function getAssetFilename(asset: AssetResponseDto): string { const fileExtension = getFilenameExtension(asset.originalPath); return `${asset.originalFileName}.${fileExtension}`; } function isRotated90CW(orientation: number) { return orientation === 5 || orientation === 6 || orientation === 90; } function isRotated270CW(orientation: number) { return orientation === 7 || orientation === 8 || orientation === -90; } /** * Returns aspect ratio for the asset */ export function getAssetRatio(asset: AssetResponseDto) { let height = asset.exifInfo?.exifImageHeight || 235; let width = asset.exifInfo?.exifImageWidth || 235; const orientation = Number(asset.exifInfo?.orientation); if (orientation) { if (isRotated90CW(orientation) || isRotated270CW(orientation)) { [width, height] = [height, width]; } } return { width, height }; } // list of supported image extensions from https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types excluding svg const supportedImageExtensions = new Set(['apng', 'avif', 'gif', 'jpg', 'jpeg', 'jfif', 'pjpeg', 'pjp', 'png', 'webp']); /** * Returns true if the asset is an image supported by web browsers, false otherwise */ export function isWebCompatibleImage(asset: AssetResponseDto): boolean { const imgExtension = getFilenameExtension(asset.originalPath); return supportedImageExtensions.has(imgExtension); } export const getAssetType = (type: AssetTypeEnum) => { switch (type) { case 'IMAGE': return 'Photo'; case 'VIDEO': return 'Video'; default: return 'Asset'; } }; export const getSelectedAssets = (assets: Set, user: UserResponseDto | null): string[] => { const ids = Array.from(assets) .filter((a) => !a.isExternal && user && a.ownerId === user.id) .map((a) => a.id); const numberOfIssues = Array.from(assets).filter((a) => a.isExternal || (user && a.ownerId !== user.id)).length; if (numberOfIssues > 0) { notificationController.show({ message: `Can't change metadata of ${numberOfIssues} asset${numberOfIssues > 1 ? 's' : ''}`, type: NotificationType.Warning, }); } return ids; };