mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 12:15:47 -04:00
parent
233372303b
commit
01c7adc24d
@ -577,6 +577,16 @@ describe('/asset', () => {
|
|||||||
expect(body).toMatchObject({ id: user1Assets[0].id, livePhotoVideoId: asset.id });
|
expect(body).toMatchObject({ id: user1Assets[0].id, livePhotoVideoId: asset.id });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should unlink a motion photo', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/assets/${user1Assets[0].id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ livePhotoVideoId: null });
|
||||||
|
|
||||||
|
expect(status).toEqual(200);
|
||||||
|
expect(body).toMatchObject({ id: user1Assets[0].id, livePhotoVideoId: null });
|
||||||
|
});
|
||||||
|
|
||||||
it('should update date time original when sidecar file contains DateTimeOriginal', async () => {
|
it('should update date time original when sidecar file contains DateTimeOriginal', async () => {
|
||||||
const sidecarData = `<?xpacket begin='?' id='W5M0MpCehiHzreSzNTczkc9d'?>
|
const sidecarData = `<?xpacket begin='?' id='W5M0MpCehiHzreSzNTczkc9d'?>
|
||||||
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 12.40'>
|
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 12.40'>
|
||||||
|
6
mobile/openapi/lib/model/update_asset_dto.dart
generated
6
mobile/openapi/lib/model/update_asset_dto.dart
generated
@ -63,12 +63,6 @@ class UpdateAssetDto {
|
|||||||
///
|
///
|
||||||
num? latitude;
|
num? latitude;
|
||||||
|
|
||||||
///
|
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
|
||||||
/// source code must fall back to having a nullable type.
|
|
||||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
|
||||||
///
|
|
||||||
String? livePhotoVideoId;
|
String? livePhotoVideoId;
|
||||||
|
|
||||||
///
|
///
|
||||||
|
@ -12243,6 +12243,7 @@
|
|||||||
},
|
},
|
||||||
"livePhotoVideoId": {
|
"livePhotoVideoId": {
|
||||||
"format": "uuid",
|
"format": "uuid",
|
||||||
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"longitude": {
|
"longitude": {
|
||||||
|
@ -427,7 +427,7 @@ export type UpdateAssetDto = {
|
|||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
livePhotoVideoId?: string;
|
livePhotoVideoId?: string | null;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
};
|
};
|
||||||
|
@ -69,8 +69,8 @@ export class UpdateAssetDto extends UpdateAssetBase {
|
|||||||
@IsString()
|
@IsString()
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
@ValidateUUID({ optional: true })
|
@ValidateUUID({ optional: true, nullable: true })
|
||||||
livePhotoVideoId?: string;
|
livePhotoVideoId?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RandomAssetsDto {
|
export class RandomAssetsDto {
|
||||||
|
@ -21,6 +21,7 @@ type EmitEventMap = {
|
|||||||
'asset.tag': [{ assetId: string }];
|
'asset.tag': [{ assetId: string }];
|
||||||
'asset.untag': [{ assetId: string }];
|
'asset.untag': [{ assetId: string }];
|
||||||
'asset.hide': [{ assetId: string; userId: string }];
|
'asset.hide': [{ assetId: string; userId: string }];
|
||||||
|
'asset.show': [{ assetId: string; userId: string }];
|
||||||
|
|
||||||
// session events
|
// session events
|
||||||
'session.delete': [{ sessionId: string }];
|
'session.delete': [{ sessionId: string }];
|
||||||
|
@ -120,6 +120,7 @@ export interface IBaseJob {
|
|||||||
export interface IEntityJob extends IBaseJob {
|
export interface IEntityJob extends IBaseJob {
|
||||||
id: string;
|
id: string;
|
||||||
source?: 'upload' | 'sidecar-write' | 'copy';
|
source?: 'upload' | 'sidecar-write' | 'copy';
|
||||||
|
notify?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IAssetDeleteJob extends IEntityJob {
|
export interface IAssetDeleteJob extends IEntityJob {
|
||||||
|
@ -39,7 +39,7 @@ import { IStackRepository } from 'src/interfaces/stack.interface';
|
|||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
import { requireAccess } from 'src/utils/access';
|
import { requireAccess } from 'src/utils/access';
|
||||||
import { getAssetFiles, getMyPartnerIds, onBeforeLink } from 'src/utils/asset.util';
|
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
export class AssetService {
|
export class AssetService {
|
||||||
@ -159,17 +159,26 @@ export class AssetService {
|
|||||||
await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
||||||
|
|
||||||
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
||||||
|
const repos = { asset: this.assetRepository, event: this.eventRepository };
|
||||||
|
|
||||||
|
let previousMotion: AssetEntity | null = null;
|
||||||
if (rest.livePhotoVideoId) {
|
if (rest.livePhotoVideoId) {
|
||||||
await onBeforeLink(
|
await onBeforeLink(repos, { userId: auth.user.id, livePhotoVideoId: rest.livePhotoVideoId });
|
||||||
{ asset: this.assetRepository, event: this.eventRepository },
|
} else if (rest.livePhotoVideoId === null) {
|
||||||
{ userId: auth.user.id, livePhotoVideoId: rest.livePhotoVideoId },
|
const asset = await this.findOrFail(id);
|
||||||
);
|
if (asset.livePhotoVideoId) {
|
||||||
|
previousMotion = await onBeforeUnlink(repos, { livePhotoVideoId: asset.livePhotoVideoId });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating });
|
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating });
|
||||||
|
|
||||||
await this.assetRepository.update({ id, ...rest });
|
await this.assetRepository.update({ id, ...rest });
|
||||||
|
|
||||||
|
if (previousMotion) {
|
||||||
|
await onAfterUnlink(repos, { userId: auth.user.id, livePhotoVideoId: previousMotion.id });
|
||||||
|
}
|
||||||
|
|
||||||
const asset = await this.assetRepository.getById(id, {
|
const asset = await this.assetRepository.getById(id, {
|
||||||
exifInfo: true,
|
exifInfo: true,
|
||||||
owner: true,
|
owner: true,
|
||||||
@ -180,9 +189,11 @@ export class AssetService {
|
|||||||
},
|
},
|
||||||
files: true,
|
files: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
throw new BadRequestException('Asset not found');
|
throw new BadRequestException('Asset not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return mapAsset(asset, { auth });
|
return mapAsset(asset, { auth });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -326,6 +337,14 @@ export class AssetService {
|
|||||||
await this.jobRepository.queueAll(jobs);
|
await this.jobRepository.queueAll(jobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async findOrFail(id: string) {
|
||||||
|
const asset = await this.assetRepository.getById(id);
|
||||||
|
if (!asset) {
|
||||||
|
throw new BadRequestException('Asset not found');
|
||||||
|
}
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
|
||||||
private async updateMetadata(dto: ISidecarWriteJob) {
|
private async updateMetadata(dto: ISidecarWriteJob) {
|
||||||
const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto;
|
const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto;
|
||||||
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined);
|
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined);
|
||||||
|
@ -289,7 +289,7 @@ export class JobService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case JobName.GENERATE_THUMBNAIL: {
|
case JobName.GENERATE_THUMBNAIL: {
|
||||||
if (item.data.source !== 'upload') {
|
if (!(item.data.notify || item.data.source === 'upload')) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +64,11 @@ export class NotificationService {
|
|||||||
this.eventRepository.clientSend(ClientEvent.ASSET_HIDDEN, userId, assetId);
|
this.eventRepository.clientSend(ClientEvent.ASSET_HIDDEN, userId, assetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OnEmit({ event: 'asset.show' })
|
||||||
|
async onAssetShow({ assetId }: ArgOf<'asset.show'>) {
|
||||||
|
await this.jobRepository.queue({ name: JobName.GENERATE_THUMBNAIL, data: { id: assetId, notify: true } });
|
||||||
|
}
|
||||||
|
|
||||||
@OnEmit({ event: 'user.signup' })
|
@OnEmit({ event: 'user.signup' })
|
||||||
async onUserSignup({ notify, id, tempPassword }: ArgOf<'user.signup'>) {
|
async onUserSignup({ notify, id, tempPassword }: ArgOf<'user.signup'>) {
|
||||||
if (notify) {
|
if (notify) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
import { AssetFileEntity } from 'src/entities/asset-files.entity';
|
||||||
@ -134,8 +135,10 @@ export const getMyPartnerIds = async ({ userId, repository, timelineEnabled }: P
|
|||||||
return [...partnerIds];
|
return [...partnerIds];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AssetHookRepositories = { asset: IAssetRepository; event: IEventRepository };
|
||||||
|
|
||||||
export const onBeforeLink = async (
|
export const onBeforeLink = async (
|
||||||
{ asset: assetRepository, event: eventRepository }: { asset: IAssetRepository; event: IEventRepository },
|
{ asset: assetRepository, event: eventRepository }: AssetHookRepositories,
|
||||||
{ userId, livePhotoVideoId }: { userId: string; livePhotoVideoId: string },
|
{ userId, livePhotoVideoId }: { userId: string; livePhotoVideoId: string },
|
||||||
) => {
|
) => {
|
||||||
const motionAsset = await assetRepository.getById(livePhotoVideoId);
|
const motionAsset = await assetRepository.getById(livePhotoVideoId);
|
||||||
@ -154,3 +157,27 @@ export const onBeforeLink = async (
|
|||||||
await eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId });
|
await eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const onBeforeUnlink = async (
|
||||||
|
{ asset: assetRepository }: AssetHookRepositories,
|
||||||
|
{ livePhotoVideoId }: { livePhotoVideoId: string },
|
||||||
|
) => {
|
||||||
|
const motion = await assetRepository.getById(livePhotoVideoId);
|
||||||
|
if (!motion) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StorageCore.isAndroidMotionPath(motion.originalPath)) {
|
||||||
|
throw new BadRequestException('Cannot unlink Android motion photos');
|
||||||
|
}
|
||||||
|
|
||||||
|
return motion;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const onAfterUnlink = async (
|
||||||
|
{ asset: assetRepository, event: eventRepository }: AssetHookRepositories,
|
||||||
|
{ userId, livePhotoVideoId }: { userId: string; livePhotoVideoId: string },
|
||||||
|
) => {
|
||||||
|
await assetRepository.update({ id: livePhotoVideoId, isVisible: true });
|
||||||
|
await eventRepository.emit('asset.show', { assetId: livePhotoVideoId, userId });
|
||||||
|
};
|
||||||
|
@ -1,44 +1,75 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
import type { OnLink } from '$lib/utils/actions';
|
import type { OnLink, OnUnlink } from '$lib/utils/actions';
|
||||||
import { AssetTypeEnum, updateAsset } from '@immich/sdk';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { mdiMotionPlayOutline, mdiTimerSand } from '@mdi/js';
|
import { AssetTypeEnum, getAssetInfo, updateAsset } from '@immich/sdk';
|
||||||
|
import { mdiLinkOff, mdiMotionPlayOutline, mdiTimerSand } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
|
||||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
|
|
||||||
export let onLink: OnLink;
|
export let onLink: OnLink;
|
||||||
|
export let onUnlink: OnUnlink;
|
||||||
export let menuItem = false;
|
export let menuItem = false;
|
||||||
|
export let unlink = false;
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|
||||||
const text = $t('link_motion_video');
|
$: text = unlink ? $t('unlink_motion_video') : $t('link_motion_video');
|
||||||
const icon = mdiMotionPlayOutline;
|
$: icon = unlink ? mdiLinkOff : mdiMotionPlayOutline;
|
||||||
|
|
||||||
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
const { clearSelect, getOwnedAssets } = getAssetControlContext();
|
||||||
|
|
||||||
|
const onClick = () => (unlink ? handleUnlink() : handleLink());
|
||||||
|
|
||||||
const handleLink = async () => {
|
const handleLink = async () => {
|
||||||
let [still, motion] = [...getOwnedAssets()];
|
let [still, motion] = [...getOwnedAssets()];
|
||||||
if (still.type === AssetTypeEnum.Video) {
|
if (still.type === AssetTypeEnum.Video) {
|
||||||
[still, motion] = [motion, still];
|
[still, motion] = [motion, still];
|
||||||
}
|
}
|
||||||
|
|
||||||
loading = true;
|
try {
|
||||||
const response = await updateAsset({ id: still.id, updateAssetDto: { livePhotoVideoId: motion.id } });
|
loading = true;
|
||||||
onLink(response);
|
const stillResponse = await updateAsset({ id: still.id, updateAssetDto: { livePhotoVideoId: motion.id } });
|
||||||
clearSelect();
|
onLink({ still: stillResponse, motion });
|
||||||
loading = false;
|
clearSelect();
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_link_motion_video'));
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnlink = async () => {
|
||||||
|
const [still] = [...getOwnedAssets()];
|
||||||
|
|
||||||
|
const motionId = still?.livePhotoVideoId;
|
||||||
|
if (!motionId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loading = true;
|
||||||
|
const stillResponse = await updateAsset({ id: still.id, updateAssetDto: { livePhotoVideoId: null } });
|
||||||
|
const motionResponse = await getAssetInfo({ id: motionId });
|
||||||
|
onUnlink({ still: stillResponse, motion: motionResponse });
|
||||||
|
clearSelect();
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_unlink_motion_video'));
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if menuItem}
|
{#if menuItem}
|
||||||
<MenuOption {text} {icon} onClick={handleLink} />
|
<MenuOption {text} {icon} {onClick} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !menuItem}
|
{#if !menuItem}
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
|
<CircleIconButton title={$t('loading')} icon={mdiTimerSand} />
|
||||||
{:else}
|
{:else}
|
||||||
<CircleIconButton title={text} {icon} on:click={handleLink} />
|
<CircleIconButton title={text} {icon} on:click={onClick} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -641,6 +641,7 @@
|
|||||||
"unable_to_get_comments_number": "Unable to get number of comments",
|
"unable_to_get_comments_number": "Unable to get number of comments",
|
||||||
"unable_to_get_shared_link": "Failed to get shared link",
|
"unable_to_get_shared_link": "Failed to get shared link",
|
||||||
"unable_to_hide_person": "Unable to hide person",
|
"unable_to_hide_person": "Unable to hide person",
|
||||||
|
"unable_to_link_motion_video": "Unable to link motion video",
|
||||||
"unable_to_link_oauth_account": "Unable to link OAuth account",
|
"unable_to_link_oauth_account": "Unable to link OAuth account",
|
||||||
"unable_to_load_album": "Unable to load album",
|
"unable_to_load_album": "Unable to load album",
|
||||||
"unable_to_load_asset_activity": "Unable to load asset activity",
|
"unable_to_load_asset_activity": "Unable to load asset activity",
|
||||||
@ -679,6 +680,7 @@
|
|||||||
"unable_to_submit_job": "Unable to submit job",
|
"unable_to_submit_job": "Unable to submit job",
|
||||||
"unable_to_trash_asset": "Unable to trash asset",
|
"unable_to_trash_asset": "Unable to trash asset",
|
||||||
"unable_to_unlink_account": "Unable to unlink account",
|
"unable_to_unlink_account": "Unable to unlink account",
|
||||||
|
"unable_to_unlink_motion_video": "Unable to unlink motion video",
|
||||||
"unable_to_update_album_cover": "Unable to update album cover",
|
"unable_to_update_album_cover": "Unable to update album cover",
|
||||||
"unable_to_update_album_info": "Unable to update album info",
|
"unable_to_update_album_info": "Unable to update album info",
|
||||||
"unable_to_update_library": "Unable to update library",
|
"unable_to_update_library": "Unable to update library",
|
||||||
@ -1219,6 +1221,7 @@
|
|||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"unknown_year": "Unknown Year",
|
"unknown_year": "Unknown Year",
|
||||||
"unlimited": "Unlimited",
|
"unlimited": "Unlimited",
|
||||||
|
"unlink_motion_video": "Unlink motion video",
|
||||||
"unlink_oauth": "Unlink OAuth",
|
"unlink_oauth": "Unlink OAuth",
|
||||||
"unlinked_oauth_account": "Unlinked OAuth account",
|
"unlinked_oauth_account": "Unlinked OAuth account",
|
||||||
"unnamed_album": "Unnamed Album",
|
"unnamed_album": "Unnamed Album",
|
||||||
|
@ -6,7 +6,8 @@ import { handleError } from './handle-error';
|
|||||||
|
|
||||||
export type OnDelete = (assetIds: string[]) => void;
|
export type OnDelete = (assetIds: string[]) => void;
|
||||||
export type OnRestore = (ids: string[]) => void;
|
export type OnRestore = (ids: string[]) => void;
|
||||||
export type OnLink = (asset: AssetResponseDto) => void;
|
export type OnLink = (assets: { still: AssetResponseDto; motion: AssetResponseDto }) => void;
|
||||||
|
export type OnUnlink = (assets: { still: AssetResponseDto; motion: AssetResponseDto }) => void;
|
||||||
export type OnArchive = (ids: string[], isArchived: boolean) => void;
|
export type OnArchive = (ids: string[], isArchived: boolean) => void;
|
||||||
export type OnFavorite = (ids: string[], favorite: boolean) => void;
|
export type OnFavorite = (ids: string[], favorite: boolean) => void;
|
||||||
export type OnStack = (ids: string[]) => void;
|
export type OnStack = (ids: string[]) => void;
|
||||||
|
@ -23,8 +23,9 @@
|
|||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { AssetStore } from '$lib/stores/assets.store';
|
import { AssetStore } from '$lib/stores/assets.store';
|
||||||
import { preferences, user } from '$lib/stores/user.store';
|
import { preferences, user } from '$lib/stores/user.store';
|
||||||
|
import type { OnLink, OnUnlink } from '$lib/utils/actions';
|
||||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
import { AssetTypeEnum } from '@immich/sdk';
|
||||||
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@ -35,12 +36,21 @@
|
|||||||
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
|
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
|
||||||
|
|
||||||
let isAllFavorite: boolean;
|
let isAllFavorite: boolean;
|
||||||
|
let isAllOwned: boolean;
|
||||||
let isAssetStackSelected: boolean;
|
let isAssetStackSelected: boolean;
|
||||||
|
let isLinkActionAvailable: boolean;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
const selection = [...$selectedAssets];
|
const selection = [...$selectedAssets];
|
||||||
|
isAllOwned = selection.every((asset) => asset.ownerId === $user.id);
|
||||||
isAllFavorite = selection.every((asset) => asset.isFavorite);
|
isAllFavorite = selection.every((asset) => asset.isFavorite);
|
||||||
isAssetStackSelected = selection.length === 1 && !!selection[0].stack;
|
isAssetStackSelected = selection.length === 1 && !!selection[0].stack;
|
||||||
|
const isLivePhoto = selection.length === 1 && !!selection[0].livePhotoVideoId;
|
||||||
|
const isLivePhotoCandidate =
|
||||||
|
selection.length === 2 &&
|
||||||
|
selection.some((asset) => asset.type === AssetTypeEnum.Image) &&
|
||||||
|
selection.some((asset) => asset.type === AssetTypeEnum.Image);
|
||||||
|
isLinkActionAvailable = isAllOwned && (isLivePhoto || isLivePhotoCandidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEscape = () => {
|
const handleEscape = () => {
|
||||||
@ -53,11 +63,14 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLink = (asset: AssetResponseDto) => {
|
const handleLink: OnLink = ({ still, motion }) => {
|
||||||
if (asset.livePhotoVideoId) {
|
assetStore.removeAssets([motion.id]);
|
||||||
assetStore.removeAssets([asset.livePhotoVideoId]);
|
assetStore.updateAssets([still]);
|
||||||
}
|
};
|
||||||
assetStore.updateAssets([asset]);
|
|
||||||
|
const handleUnlink: OnUnlink = ({ still, motion }) => {
|
||||||
|
assetStore.addAssets([motion]);
|
||||||
|
assetStore.updateAssets([still]);
|
||||||
};
|
};
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
@ -87,8 +100,13 @@
|
|||||||
onUnstack={(assets) => assetStore.addAssets(assets)}
|
onUnstack={(assets) => assetStore.addAssets(assets)}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if $selectedAssets.size === 2 && [...$selectedAssets].some((asset) => asset.type === AssetTypeEnum.Image && [...$selectedAssets].some((asset) => asset.type === AssetTypeEnum.Video))}
|
{#if isLinkActionAvailable}
|
||||||
<LinkLivePhotoAction menuItem onLink={handleLink} />
|
<LinkLivePhotoAction
|
||||||
|
menuItem
|
||||||
|
unlink={[...$selectedAssets].length === 1}
|
||||||
|
onLink={handleLink}
|
||||||
|
onUnlink={handleUnlink}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<ChangeDate menuItem />
|
<ChangeDate menuItem />
|
||||||
<ChangeLocation menuItem />
|
<ChangeLocation menuItem />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user