mirror of
https://github.com/immich-app/immich.git
synced 2026-01-15 22:42:31 -08:00
refactor: download action (#25124)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { isEnabled } from '$lib/utils';
|
||||
import { IconButton, type ActionItem } from '@immich/ui';
|
||||
|
||||
type Props = {
|
||||
@@ -9,6 +10,6 @@
|
||||
const { title, icon, color = 'secondary', onAction } = $derived(action);
|
||||
</script>
|
||||
|
||||
{#if icon && (action.$if?.() ?? true)}
|
||||
{#if icon && isEnabled(action)}
|
||||
<IconButton variant="ghost" shape="round" {color} {icon} aria-label={title} onclick={() => onAction(action)} />
|
||||
{/if}
|
||||
|
||||
16
web/src/lib/components/ActionMenuItem.svelte
Normal file
16
web/src/lib/components/ActionMenuItem.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { isEnabled } from '$lib/utils';
|
||||
import { type ActionItem } from '@immich/ui';
|
||||
|
||||
type Props = {
|
||||
action: ActionItem;
|
||||
};
|
||||
|
||||
const { action }: Props = $props();
|
||||
const { title, icon, onAction } = $derived(action);
|
||||
</script>
|
||||
|
||||
{#if icon && isEnabled(action)}
|
||||
<MenuOption {icon} text={title} onClick={() => onAction(action)} />
|
||||
{/if}
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { downloadFile } from '$lib/utils/asset-utils';
|
||||
import { getAssetInfo } from '@immich/sdk';
|
||||
import { IconButton } from '@immich/ui';
|
||||
import { mdiDownload } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
asset: TimelineAsset;
|
||||
menuItem?: boolean;
|
||||
}
|
||||
|
||||
let { asset, menuItem = false }: Props = $props();
|
||||
|
||||
const onDownloadFile = async () => downloadFile(await getAssetInfo({ ...authManager.params, id: asset.id }));
|
||||
</script>
|
||||
|
||||
<svelte:document use:shortcut={{ shortcut: { key: 'd', shift: true }, onShortcut: onDownloadFile }} />
|
||||
|
||||
{#if !menuItem}
|
||||
<IconButton
|
||||
color="secondary"
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
icon={mdiDownload}
|
||||
aria-label={$t('download')}
|
||||
onclick={onDownloadFile}
|
||||
/>
|
||||
{:else}
|
||||
<MenuOption icon={mdiDownload} text={$t('download')} onClick={onDownloadFile} />
|
||||
{/if}
|
||||
@@ -2,12 +2,12 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import ActionButton from '$lib/components/ActionButton.svelte';
|
||||
import ActionMenuItem from '$lib/components/ActionMenuItem.svelte';
|
||||
import type { OnAction, PreAction } from '$lib/components/asset-viewer/actions/action';
|
||||
import AddToAlbumAction from '$lib/components/asset-viewer/actions/add-to-album-action.svelte';
|
||||
import AddToStackAction from '$lib/components/asset-viewer/actions/add-to-stack-action.svelte';
|
||||
import ArchiveAction from '$lib/components/asset-viewer/actions/archive-action.svelte';
|
||||
import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte';
|
||||
import DownloadAction from '$lib/components/asset-viewer/actions/download-action.svelte';
|
||||
import KeepThisDeleteOthersAction from '$lib/components/asset-viewer/actions/keep-this-delete-others.svelte';
|
||||
import RatingAction from '$lib/components/asset-viewer/actions/rating-action.svelte';
|
||||
import RemoveAssetFromStack from '$lib/components/asset-viewer/actions/remove-asset-from-stack.svelte';
|
||||
@@ -27,7 +27,7 @@
|
||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||
import { getAssetJobName, getSharedLink, withoutIcons } from '$lib/utils';
|
||||
import { getAssetJobName, withoutIcons } from '$lib/utils';
|
||||
import type { OnUndoDelete } from '$lib/utils/actions';
|
||||
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
@@ -96,9 +96,7 @@
|
||||
setPlayOriginalVideo,
|
||||
}: Props = $props();
|
||||
|
||||
const sharedLink = getSharedLink();
|
||||
let isOwner = $derived($user && asset.ownerId === $user?.id);
|
||||
let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline);
|
||||
let isLocked = $derived(asset.visibility === AssetVisibility.Locked);
|
||||
let smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch);
|
||||
|
||||
@@ -113,9 +111,8 @@
|
||||
|
||||
const { Cast } = $derived(getGlobalActions($t));
|
||||
|
||||
const { Share, Offline, Favorite, Unfavorite, PlayMotionPhoto, StopMotionPhoto, Info } = $derived(
|
||||
getAssetActions($t, asset),
|
||||
);
|
||||
const { Share, Download, SharedLinkDownload, Offline, Favorite, Unfavorite, PlayMotionPhoto, StopMotionPhoto, Info } =
|
||||
$derived(getAssetActions($t, asset));
|
||||
|
||||
// $: showEditorButton =
|
||||
// isOwner &&
|
||||
@@ -169,10 +166,7 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !isOwner && showDownloadButton}
|
||||
<DownloadAction asset={toTimelineAsset(asset)} />
|
||||
{/if}
|
||||
|
||||
<ActionButton action={SharedLinkDownload} />
|
||||
<ActionButton action={Info} />
|
||||
<ActionButton action={Favorite} />
|
||||
<ActionButton action={Unfavorite} />
|
||||
@@ -188,9 +182,8 @@
|
||||
{#if showSlideshow && !isLocked}
|
||||
<MenuOption icon={mdiPresentationPlay} text={$t('slideshow')} onClick={onPlaySlideshow} />
|
||||
{/if}
|
||||
{#if showDownloadButton}
|
||||
<DownloadAction asset={toTimelineAsset(asset)} menuItem />
|
||||
{/if}
|
||||
|
||||
<ActionMenuItem action={Download} />
|
||||
|
||||
{#if !isLocked}
|
||||
{#if asset.isTrashed}
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
import { shortcut as bindShortcut, shortcutLabel as computeShortcutLabel } from '$lib/actions/shortcut';
|
||||
import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store';
|
||||
import { generateId } from '$lib/utils/generate-id';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { Icon, type IconLike } from '@immich/ui';
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
subtitle?: string;
|
||||
icon?: string;
|
||||
icon?: IconLike;
|
||||
activeColor?: string;
|
||||
textColor?: string;
|
||||
onClick: () => void;
|
||||
@@ -19,7 +19,7 @@
|
||||
let {
|
||||
text,
|
||||
subtitle = '',
|
||||
icon = '',
|
||||
icon,
|
||||
activeColor = 'bg-slate-300',
|
||||
textColor = 'text-immich-fg dark:text-immich-dark-bg',
|
||||
onClick,
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { downloadArchive, downloadFile } from '$lib/utils/asset-utils';
|
||||
import { handleDownloadAsset } from '$lib/services/asset.service';
|
||||
import { downloadArchive } from '$lib/utils/asset-utils';
|
||||
import { getAssetInfo } from '@immich/sdk';
|
||||
import { IconButton } from '@immich/ui';
|
||||
import { mdiDownload } from '@mdi/js';
|
||||
@@ -24,7 +25,7 @@
|
||||
if (assets.length === 1) {
|
||||
clearSelect();
|
||||
let asset = await getAssetInfo({ ...authManager.params, id: assets[0].id });
|
||||
await downloadFile(asset);
|
||||
await handleDownloadAsset(asset);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,27 @@
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { user as authUser } from '$lib/stores/user.store';
|
||||
import { user as authUser, preferences } from '$lib/stores/user.store';
|
||||
import { getSharedLink, sleep } from '$lib/utils';
|
||||
import { downloadUrl } from '$lib/utils/asset-utils';
|
||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { AssetVisibility, copyAsset, deleteAssets, updateAsset, type AssetResponseDto } from '@immich/sdk';
|
||||
import { asQueryString } from '$lib/utils/shared-links';
|
||||
import {
|
||||
AssetVisibility,
|
||||
copyAsset,
|
||||
deleteAssets,
|
||||
getAssetInfo,
|
||||
getBaseUrl,
|
||||
updateAsset,
|
||||
type AssetResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||
import {
|
||||
mdiAlertOutline,
|
||||
mdiDownload,
|
||||
mdiHeart,
|
||||
mdiHeartOutline,
|
||||
mdiInformationOutline,
|
||||
@@ -20,6 +33,7 @@ import type { MessageFormatter } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => {
|
||||
const sharedLink = getSharedLink();
|
||||
const currentAuthUser = get(authUser);
|
||||
const isOwner = !!(currentAuthUser && currentAuthUser.id === asset.ownerId);
|
||||
|
||||
@@ -31,6 +45,20 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
onAction: () => modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] }),
|
||||
};
|
||||
|
||||
const Download: ActionItem = {
|
||||
title: $t('download'),
|
||||
icon: mdiDownload,
|
||||
shortcuts: { key: 'd', shift: true },
|
||||
type: $t('assets'),
|
||||
$if: () => !!currentAuthUser,
|
||||
onAction: () => handleDownloadAsset(asset),
|
||||
};
|
||||
|
||||
const SharedLinkDownload: ActionItem = {
|
||||
...Download,
|
||||
$if: () => !currentAuthUser && sharedLink && sharedLink.allowDownload,
|
||||
};
|
||||
|
||||
const PlayMotionPhoto: ActionItem = {
|
||||
title: $t('play_motion_photo'),
|
||||
icon: mdiMotionPlayOutline,
|
||||
@@ -87,7 +115,50 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
shortcuts: [{ key: 'i' }],
|
||||
};
|
||||
|
||||
return { Share, Offline, Info, Favorite, Unfavorite, PlayMotionPhoto, StopMotionPhoto };
|
||||
return { Share, Download, SharedLinkDownload, Offline, Info, Favorite, Unfavorite, PlayMotionPhoto, StopMotionPhoto };
|
||||
};
|
||||
|
||||
export const handleDownloadAsset = async (asset: AssetResponseDto) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
const assets = [
|
||||
{
|
||||
filename: asset.originalFileName,
|
||||
id: asset.id,
|
||||
size: asset.exifInfo?.fileSizeInByte || 0,
|
||||
},
|
||||
];
|
||||
|
||||
const isAndroidMotionVideo = (asset: AssetResponseDto) => {
|
||||
return asset.originalPath.includes('encoded-video');
|
||||
};
|
||||
|
||||
if (asset.livePhotoVideoId) {
|
||||
const motionAsset = await getAssetInfo({ ...authManager.params, id: asset.livePhotoVideoId });
|
||||
if (!isAndroidMotionVideo(motionAsset) || get(preferences)?.download.includeEmbeddedVideos) {
|
||||
assets.push({
|
||||
filename: motionAsset.originalFileName,
|
||||
id: asset.livePhotoVideoId,
|
||||
size: motionAsset.exifInfo?.fileSizeInByte || 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const queryParams = asQueryString(authManager.params);
|
||||
|
||||
for (const [i, { filename, id }] of assets.entries()) {
|
||||
if (i !== 0) {
|
||||
// play nice with Safari
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
try {
|
||||
toastManager.success($t('downloading_asset_filename', { values: { filename: asset.originalFileName } }));
|
||||
downloadUrl(getBaseUrl() + `/assets/${id}/original` + (queryParams ? `?${queryParams}` : ''), filename);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.error_downloading', { values: { filename } }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFavorite = async (asset: AssetResponseDto) => {
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
type SharedLinkResponseDto,
|
||||
type UserResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { toastManager, type ActionItem } from '@immich/ui';
|
||||
import { toastManager, type ActionItem, type IfLike } from '@immich/ui';
|
||||
import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiHeadSyncOutline, mdiImageRefreshOutline } from '@mdi/js';
|
||||
import { init, register, t } from 'svelte-i18n';
|
||||
import { derived, get } from 'svelte/store';
|
||||
@@ -443,3 +443,5 @@ export const semverToName = ({ major, minor, patch }: ServerVersionResponseDto)
|
||||
|
||||
export const withoutIcons = (actions: ActionItem[]): ActionItem[] =>
|
||||
actions.map((action) => ({ ...action, icon: undefined }));
|
||||
|
||||
export const isEnabled = ({ $if }: IfLike) => $if?.() ?? true;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import { downloadRequest, sleep, withError } from '$lib/utils';
|
||||
import { downloadRequest, withError } from '$lib/utils';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
createStack,
|
||||
deleteAssets,
|
||||
deleteStacks,
|
||||
getAssetInfo,
|
||||
getBaseUrl,
|
||||
getDownloadInfo,
|
||||
getStack,
|
||||
@@ -232,48 +231,6 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn
|
||||
}
|
||||
};
|
||||
|
||||
export const downloadFile = async (asset: AssetResponseDto) => {
|
||||
const $t = get(t);
|
||||
const assets = [
|
||||
{
|
||||
filename: asset.originalFileName,
|
||||
id: asset.id,
|
||||
size: asset.exifInfo?.fileSizeInByte || 0,
|
||||
},
|
||||
];
|
||||
|
||||
const isAndroidMotionVideo = (asset: AssetResponseDto) => {
|
||||
return asset.originalPath.includes('encoded-video');
|
||||
};
|
||||
|
||||
if (asset.livePhotoVideoId) {
|
||||
const motionAsset = await getAssetInfo({ ...authManager.params, id: asset.livePhotoVideoId });
|
||||
if (!isAndroidMotionVideo(motionAsset) || get(preferences)?.download.includeEmbeddedVideos) {
|
||||
assets.push({
|
||||
filename: motionAsset.originalFileName,
|
||||
id: asset.livePhotoVideoId,
|
||||
size: motionAsset.exifInfo?.fileSizeInByte || 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const queryParams = asQueryString(authManager.params);
|
||||
|
||||
for (const [i, { filename, id }] of assets.entries()) {
|
||||
if (i !== 0) {
|
||||
// play nice with Safari
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
try {
|
||||
toastManager.success($t('downloading_asset_filename', { values: { filename: asset.originalFileName } }));
|
||||
downloadUrl(getBaseUrl() + `/assets/${id}/original` + (queryParams ? `?${queryParams}` : ''), filename);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.error_downloading', { values: { filename } }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the lowercase filename extension without a dot (.) and
|
||||
* an empty string when not found.
|
||||
|
||||
Reference in New Issue
Block a user