chore(web): refactor replace asset (#23972)

This commit is contained in:
Daniel Dietzler 2025-11-19 15:49:15 +01:00 committed by GitHub
parent 11cec56e80
commit f59417cc77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 43 additions and 34 deletions

View File

@ -25,12 +25,12 @@
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { AppRoute } from '$lib/constants';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { handleReplaceAsset } from '$lib/services/asset.service';
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 } from '$lib/utils';
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import {
AssetJobName,
@ -227,7 +227,7 @@
<ArchiveAction {asset} {onAction} {preAction} />
<MenuOption
icon={mdiUpload}
onClick={() => openFileUploadDialog({ multiple: false, assetId: asset.id })}
onClick={() => handleReplaceAsset(asset.id)}
text={$t('replace_with_upload')}
/>
{#if !asset.isArchived && !asset.isTrashed}

View File

@ -1,11 +1,13 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { focusTrap } from '$lib/actions/focus-trap';
import type { Action, OnAction, PreAction } from '$lib/components/asset-viewer/actions/action';
import MotionPhotoAction from '$lib/components/asset-viewer/actions/motion-photo-action.svelte';
import NextAssetAction from '$lib/components/asset-viewer/actions/next-asset-action.svelte';
import PreviousAssetAction from '$lib/components/asset-viewer/actions/previous-asset-action.svelte';
import AssetViewerNavBar from '$lib/components/asset-viewer/asset-viewer-nav-bar.svelte';
import { AssetAction, ProjectionType } from '$lib/constants';
import OnEvents from '$lib/components/OnEvents.svelte';
import { AppRoute, AssetAction, ProjectionType } from '$lib/constants';
import { activityManager } from '$lib/managers/activity-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
@ -363,6 +365,15 @@
selectedEditType = type;
};
const handleAssetReplace = async ({ oldAssetId, newAssetId }: { oldAssetId: string; newAssetId: string }) => {
if (oldAssetId !== asset.id) {
return;
}
await new Promise((promise) => setTimeout(promise, 500));
await goto(`${AppRoute.PHOTOS}/${newAssetId}`);
};
let isFullScreen = $derived(fullscreenElement !== null);
$effect(() => {
@ -388,6 +399,8 @@
});
</script>
<OnEvents onAssetReplace={handleAssetReplace} />
<svelte:document bind:fullscreenElement />
<section

View File

@ -16,6 +16,8 @@ export type Events = {
LanguageChange: [{ name: string; code: string; rtl?: boolean }];
ThemeChange: [ThemeSetting];
AssetReplace: [{ oldAssetId: string; newAssetId: string }];
AlbumDelete: [AlbumResponseDto];
SharedLinkCreate: [SharedLinkResponseDto];

View File

@ -0,0 +1,11 @@
import { eventManager } from '$lib/managers/event-manager.svelte';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { copyAsset, deleteAssets } from '@immich/sdk';
export const handleReplaceAsset = async (oldAssetId: string) => {
const [newAssetId] = await openFileUploadDialog({ multiple: false });
await copyAsset({ assetCopyDto: { sourceId: oldAssetId, targetId: newAssetId } });
await deleteAssets({ assetBulkDeleteDto: { ids: [oldAssetId], force: true } });
eventManager.emit('AssetReplace', { oldAssetId, newAssetId });
};

View File

@ -12,7 +12,6 @@ import {
AssetMediaStatus,
AssetVisibility,
checkBulkUpload,
getAssetOriginalPath,
getBaseUrl,
type AssetMediaResponseDto,
} from '@immich/sdk';
@ -44,12 +43,10 @@ export const addDummyItems = () => {
export const uploadExecutionQueue = new ExecutorQueue({ concurrency: 2 });
type FileUploadParam = { multiple?: boolean } & (
| { albumId?: string; assetId?: never }
| { albumId?: never; assetId?: string }
);
type FileUploadParam = { multiple?: boolean; albumId?: string };
export const openFileUploadDialog = async (options: FileUploadParam = {}) => {
const { albumId, multiple = true, assetId } = options;
const { albumId, multiple = true } = options;
const extensions = uploadManager.getExtensions();
return new Promise<string[]>((resolve, reject) => {
@ -68,7 +65,7 @@ export const openFileUploadDialog = async (options: FileUploadParam = {}) => {
}
const files = Array.from(target.files);
resolve(fileUploadHandler({ files, albumId, replaceAssetId: assetId }));
resolve(fileUploadHandler({ files, albumId }));
},
{ passive: true },
);
@ -88,7 +85,6 @@ type FileUploadHandlerParams = Omit<FileUploaderParams, 'deviceAssetId' | 'asset
export const fileUploadHandler = async ({
files,
albumId,
replaceAssetId,
isLockedAssets = false,
}: FileUploadHandlerParams): Promise<string[]> => {
const extensions = uploadManager.getExtensions();
@ -99,9 +95,7 @@ export const fileUploadHandler = async ({
const deviceAssetId = getDeviceAssetId(file);
uploadAssetsStore.addItem({ id: deviceAssetId, file, albumId });
promises.push(
uploadExecutionQueue.addTask(() =>
fileUploader({ assetFile: file, deviceAssetId, albumId, replaceAssetId, isLockedAssets }),
),
uploadExecutionQueue.addTask(() => fileUploader({ assetFile: file, deviceAssetId, albumId, isLockedAssets })),
);
}
}
@ -127,7 +121,6 @@ async function fileUploader({
assetFile,
deviceAssetId,
albumId,
replaceAssetId,
isLockedAssets = false,
}: FileUploaderParams): Promise<string | undefined> {
const fileCreatedAt = new Date(assetFile.lastModified).toISOString();
@ -183,15 +176,6 @@ async function fileUploader({
const queryParams = asQueryString(authManager.params);
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_uploading') });
if (replaceAssetId) {
const response = await uploadRequest<AssetMediaResponseDto>({
url: getBaseUrl() + getAssetOriginalPath(replaceAssetId) + (queryParams ? `?${queryParams}` : ''),
method: 'PUT',
data: formData,
onUploadProgress: (event) => uploadAssetsStore.updateProgress(deviceAssetId, event.loaded, event.total),
});
responseData = response.data;
} else {
const response = await uploadRequest<AssetMediaResponseDto>({
url: getBaseUrl() + '/assets' + (queryParams ? `?${queryParams}` : ''),
data: formData,
@ -204,7 +188,6 @@ async function fileUploader({
responseData = response.data;
}
}
if (responseData.status === AssetMediaStatus.Duplicate) {
uploadAssetsStore.track('duplicate');