mirror of
https://github.com/immich-app/immich.git
synced 2026-06-23 07:06:43 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 963a4dc0da | |||
| 3fe3e0960c |
@@ -110,11 +110,11 @@
|
||||
let sharedLink = getSharedLink();
|
||||
let fullscreenElement = $state<Element>();
|
||||
|
||||
let playOriginalVideo = $state($alwaysLoadOriginalVideo);
|
||||
let isPlayingOriginalVideo = $state($alwaysLoadOriginalVideo);
|
||||
let slideshowStartAssetId = $state<string>();
|
||||
|
||||
const setPlayOriginalVideo = (value: boolean) => {
|
||||
playOriginalVideo = value;
|
||||
isPlayingOriginalVideo = value;
|
||||
};
|
||||
|
||||
const refreshStack = async () => {
|
||||
@@ -504,7 +504,7 @@
|
||||
{onUndoDelete}
|
||||
onClose={onClose ? () => onClose(stack?.primaryAssetId ?? asset.id) : undefined}
|
||||
{onRemoveFromAlbum}
|
||||
{playOriginalVideo}
|
||||
{isPlayingOriginalVideo}
|
||||
{setPlayOriginalVideo}
|
||||
/>
|
||||
</div>
|
||||
@@ -542,7 +542,7 @@
|
||||
onClose={closeViewer}
|
||||
onVideoEnded={() => navigateAsset()}
|
||||
onVideoStarted={handleVideoStarted}
|
||||
{playOriginalVideo}
|
||||
playOriginalVideo={isPlayingOriginalVideo}
|
||||
/>
|
||||
{:else if viewerKind === 'LiveVideoViewer'}
|
||||
<VideoViewer
|
||||
@@ -554,7 +554,7 @@
|
||||
onPreviousAsset={() => navigateAsset('previous')}
|
||||
onNextAsset={() => navigateAsset('next')}
|
||||
onVideoEnded={() => (assetViewerManager.isPlayingMotionPhoto = false)}
|
||||
{playOriginalVideo}
|
||||
playOriginalVideo={isPlayingOriginalVideo}
|
||||
/>
|
||||
{:else if viewerKind === 'ImagePanaramaViewer'}
|
||||
<ImagePanoramaViewer {asset} />
|
||||
@@ -574,7 +574,7 @@
|
||||
onClose={closeViewer}
|
||||
onVideoEnded={() => navigateAsset()}
|
||||
onVideoStarted={handleVideoStarted}
|
||||
{playOriginalVideo}
|
||||
playOriginalVideo={isPlayingOriginalVideo}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import ActionMenuItem from '$lib/components/ActionMenuItem.svelte';
|
||||
import type { OnAction, PreAction } from '$lib/components/asset-viewer/actions/action';
|
||||
import AddToStackAction from '$lib/components/asset-viewer/actions/AddToStackAction.svelte';
|
||||
@@ -10,19 +9,15 @@
|
||||
import RemoveAssetFromStack from '$lib/components/asset-viewer/actions/RemoveAssetFromStack.svelte';
|
||||
import RestoreAction from '$lib/components/asset-viewer/actions/RestoreAction.svelte';
|
||||
import SetFeaturedPhotoAction from '$lib/components/asset-viewer/actions/SetPersonFeaturedAction.svelte';
|
||||
import SetProfilePictureAction from '$lib/components/asset-viewer/actions/SetProfilePictureAction.svelte';
|
||||
import SetStackPrimaryAsset from '$lib/components/asset-viewer/actions/SetStackPrimaryAsset.svelte';
|
||||
import SetVisibilityAction from '$lib/components/asset-viewer/actions/SetVisibilityAction.svelte';
|
||||
import UnstackAction from '$lib/components/asset-viewer/actions/UnstackAction.svelte';
|
||||
import LoadingDots from '$lib/components/LoadingDots.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/ButtonContextMenu.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
|
||||
import RemoveFromAlbumAction from '$lib/components/timeline/actions/RemoveFromAlbumAction.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { languageManager } from '$lib/managers/language-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getAlbumAssetActions } from '$lib/services/album.service';
|
||||
import { getGlobalActions } from '$lib/services/app.service';
|
||||
import { getAssetActions } from '$lib/services/asset.service';
|
||||
@@ -38,7 +33,7 @@
|
||||
type StackResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { ActionButton, CommandPaletteDefaultProvider, Tooltip, type ActionItem } from '@immich/ui';
|
||||
import { mdiArrowLeft, mdiArrowRight, mdiCompare, mdiDotsVertical, mdiImageSearch, mdiVideoOutline } from '@mdi/js';
|
||||
import { mdiArrowLeft, mdiArrowRight, mdiDotsVertical, mdiVideoOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
@@ -51,7 +46,7 @@
|
||||
onUndoDelete?: OnUndoDelete;
|
||||
onClose?: () => void;
|
||||
onRemoveFromAlbum?: (assetIds: string[]) => void;
|
||||
playOriginalVideo: boolean;
|
||||
isPlayingOriginalVideo: boolean;
|
||||
setPlayOriginalVideo: (value: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -65,14 +60,13 @@
|
||||
onUndoDelete = undefined,
|
||||
onClose,
|
||||
onRemoveFromAlbum,
|
||||
playOriginalVideo = false,
|
||||
isPlayingOriginalVideo = false,
|
||||
setPlayOriginalVideo,
|
||||
}: Props = $props();
|
||||
|
||||
const isOwner = $derived(authManager.authenticated && asset.ownerId === authManager.user.id);
|
||||
const isAlbumOwner = $derived(authManager.authenticated && album?.albumUsers[0].user.id === authManager.user.id);
|
||||
const isLocked = $derived(asset.visibility === AssetVisibility.Locked);
|
||||
const smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch);
|
||||
|
||||
const { Cast } = $derived(getGlobalActions($t));
|
||||
|
||||
@@ -84,7 +78,14 @@
|
||||
shortcuts: [{ key: 'Escape' }],
|
||||
});
|
||||
|
||||
const Actions = $derived(getAssetActions($t, asset));
|
||||
const PlayOriginalVideo: ActionItem = $derived({
|
||||
title: isPlayingOriginalVideo ? $t('play_transcoded_video') : $t('play_original_video'),
|
||||
icon: mdiVideoOutline,
|
||||
$if: () => asset.type === AssetTypeEnum.Video,
|
||||
onAction: () => setPlayOriginalVideo(!isPlayingOriginalVideo),
|
||||
});
|
||||
|
||||
const Actions = $derived(getAssetActions($t, { ...asset, stackPrimaryAssetId: stack?.primaryAssetId }));
|
||||
const sharedLink = getSharedLink();
|
||||
</script>
|
||||
|
||||
@@ -169,41 +170,21 @@
|
||||
{#if person}
|
||||
<SetFeaturedPhotoAction {asset} {person} {onAction} />
|
||||
{/if}
|
||||
{#if asset.type === AssetTypeEnum.Image && !isLocked}
|
||||
<SetProfilePictureAction {asset} />
|
||||
{/if}
|
||||
|
||||
{#if !isLocked}
|
||||
{#if isOwner}
|
||||
<ArchiveAction {asset} {onAction} {preAction} />
|
||||
{#if !asset.isArchived && !asset.isTrashed}
|
||||
<MenuOption
|
||||
icon={mdiImageSearch}
|
||||
onClick={() => goto(Route.photos({ at: stack?.primaryAssetId ?? asset.id }))}
|
||||
text={$t('view_in_timeline')}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if !asset.isArchived && !asset.isTrashed && smartSearchEnabled}
|
||||
<MenuOption
|
||||
icon={mdiCompare}
|
||||
onClick={() => goto(Route.search({ queryAssetId: stack?.primaryAssetId ?? asset.id }))}
|
||||
text={$t('view_similar_photos')}
|
||||
/>
|
||||
{/if}
|
||||
<ActionMenuItem action={Actions.SetProfilePicture} />
|
||||
|
||||
{#if isOwner && !isLocked}
|
||||
<ArchiveAction {asset} {onAction} {preAction} />
|
||||
{/if}
|
||||
<ActionMenuItem action={Actions.ViewInTimeline} />
|
||||
<ActionMenuItem action={Actions.ViewSimilar} />
|
||||
|
||||
{#if !asset.isTrashed && isOwner}
|
||||
<SetVisibilityAction asset={toTimelineAsset(asset)} {onAction} {preAction} />
|
||||
{/if}
|
||||
|
||||
{#if asset.type === AssetTypeEnum.Video}
|
||||
<MenuOption
|
||||
icon={mdiVideoOutline}
|
||||
onClick={() => setPlayOriginalVideo(!playOriginalVideo)}
|
||||
text={playOriginalVideo ? $t('play_transcoded_video') : $t('play_original_video')}
|
||||
/>
|
||||
{/if}
|
||||
<ActionMenuItem action={PlayOriginalVideo} />
|
||||
|
||||
{#if isOwner}
|
||||
<hr />
|
||||
<ActionMenuItem action={Actions.RefreshFacesJob} />
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/MenuOption.svelte';
|
||||
import ProfileImageCropperModal from '$lib/modals/ProfileImageCropperModal.svelte';
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { modalManager } from '@immich/ui';
|
||||
import { mdiAccountCircleOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
}
|
||||
|
||||
let { asset }: Props = $props();
|
||||
</script>
|
||||
|
||||
<MenuOption
|
||||
icon={mdiAccountCircleOutline}
|
||||
onClick={() => modalManager.show(ProfileImageCropperModal, { asset })}
|
||||
text={$t('set_as_profile_picture')}
|
||||
/>
|
||||
@@ -31,6 +31,12 @@ vitest.mock('$lib/utils', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock(import('$lib/managers/feature-flags-manager.svelte'), function () {
|
||||
return {
|
||||
featureFlagsManager: { init: vi.fn(), loadFeatureFlags: vi.fn(), value: {} } as never,
|
||||
};
|
||||
});
|
||||
|
||||
describe('AssetService', () => {
|
||||
describe('getAssetActions', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -11,8 +11,10 @@ import {
|
||||
} from '@immich/sdk';
|
||||
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||
import {
|
||||
mdiAccountCircleOutline,
|
||||
mdiAlertOutline,
|
||||
mdiCogRefreshOutline,
|
||||
mdiCompare,
|
||||
mdiContentCopy,
|
||||
mdiDatabaseRefreshOutline,
|
||||
mdiDownload,
|
||||
@@ -22,6 +24,7 @@ import {
|
||||
mdiHeart,
|
||||
mdiHeartOutline,
|
||||
mdiImageRefreshOutline,
|
||||
mdiImageSearch,
|
||||
mdiInformationOutline,
|
||||
mdiMagnifyMinusOutline,
|
||||
mdiMagnifyPlusOutline,
|
||||
@@ -34,14 +37,18 @@ import {
|
||||
mdiTune,
|
||||
} from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import { ProjectionType } from '$lib/constants';
|
||||
import { assetMultiSelectManager } from '$lib/managers/asset-multi-select-manager.svelte';
|
||||
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 { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import AssetAddToAlbumModal from '$lib/modals/AssetAddToAlbumModal.svelte';
|
||||
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
|
||||
import ProfileImageCropperModal from '$lib/modals/ProfileImageCropperModal.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { getAssetMediaUrl, getSharedLink, sleep } from '$lib/utils';
|
||||
import { downloadUrl } from '$lib/utils';
|
||||
@@ -92,10 +99,11 @@ export const getAssetBulkActions = ($t: MessageFormatter) => {
|
||||
return { AddToAlbum, RefreshFacesJob, RefreshMetadataJob, RegenerateThumbnailJob, TranscodeVideoJob };
|
||||
};
|
||||
|
||||
export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => {
|
||||
export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto & { stackPrimaryAssetId?: string }) => {
|
||||
const sharedLink = getSharedLink();
|
||||
const authUser = authManager.authenticated ? authManager.user : undefined;
|
||||
const isOwner = !!(authUser && authUser.id === asset.ownerId);
|
||||
const smartSearchEnabled = featureFlagsManager.value.smartSearch;
|
||||
|
||||
const Share: ActionItem = {
|
||||
title: $t('share'),
|
||||
@@ -242,6 +250,28 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
shortcuts: [{ key: 'e' }],
|
||||
};
|
||||
|
||||
const SetProfilePicture: ActionItem = {
|
||||
title: $t('set_as_profile_picture'),
|
||||
icon: mdiAccountCircleOutline,
|
||||
$if: () => asset.type === AssetTypeEnum.Image && asset.visibility !== AssetVisibility.Locked,
|
||||
onAction: () => modalManager.show(ProfileImageCropperModal, { asset }),
|
||||
};
|
||||
|
||||
const ViewInTimeline: ActionItem = {
|
||||
title: $t('view_in_timeline'),
|
||||
icon: mdiImageSearch,
|
||||
$if: () => isOwner && asset.visibility !== AssetVisibility.Locked && !asset.isArchived && !asset.isTrashed,
|
||||
onAction: () => goto(Route.photos({ at: asset.stackPrimaryAssetId ?? asset.id })),
|
||||
};
|
||||
|
||||
const ViewSimilar: ActionItem = {
|
||||
title: $t('view_similar_photos'),
|
||||
icon: mdiCompare,
|
||||
$if: () =>
|
||||
asset.visibility !== AssetVisibility.Locked && !asset.isArchived && !asset.isTrashed && smartSearchEnabled,
|
||||
onAction: () => goto(Route.search({ queryAssetId: asset.stackPrimaryAssetId ?? asset.id })),
|
||||
};
|
||||
|
||||
const RefreshFacesJob: ActionItem = {
|
||||
title: $t('refresh_faces'),
|
||||
icon: mdiHeadSyncOutline,
|
||||
@@ -286,6 +316,9 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
Tag,
|
||||
TagPeople,
|
||||
Edit,
|
||||
SetProfilePicture,
|
||||
ViewInTimeline,
|
||||
ViewSimilar,
|
||||
RefreshFacesJob,
|
||||
RefreshMetadataJob,
|
||||
RegenerateThumbnailJob,
|
||||
|
||||
Reference in New Issue
Block a user