feat: locked/private view (#18268)

* feat: locked/private view

* feat: locked/private view

* pr feedback

* fix: redirect loop

* pr feedback
This commit is contained in:
Alex
2025-05-15 09:35:21 -06:00
committed by GitHub
parent 4935f3e0bb
commit b7b0b9b6d8
61 changed files with 1018 additions and 186 deletions

View File

@@ -13,6 +13,8 @@ type ActionMap = {
[AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto };
[AssetAction.UNSTACK]: { assets: AssetResponseDto[] };
[AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: AssetResponseDto };
[AssetAction.SET_VISIBILITY_LOCKED]: { asset: AssetResponseDto };
[AssetAction.SET_VISIBILITY_TIMELINE]: { asset: AssetResponseDto };
};
export type Action = {

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { AssetAction } from '$lib/constants';
import { modalManager } from '$lib/managers/modal-manager.svelte';
import { handleError } from '$lib/utils/handle-error';
import { AssetVisibility, updateAssets, Visibility, type AssetResponseDto } from '@immich/sdk';
import { mdiEyeOffOutline, mdiFolderMoveOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { OnAction, PreAction } from './action';
interface Props {
asset: AssetResponseDto;
onAction: OnAction;
preAction: PreAction;
}
let { asset, onAction, preAction }: Props = $props();
const isLocked = asset.visibility === Visibility.Locked;
const toggleLockedVisibility = async () => {
const isConfirmed = await modalManager.showDialog({
title: isLocked ? $t('remove_from_locked_folder') : $t('move_to_locked_folder'),
prompt: isLocked ? $t('remove_from_locked_folder_confirmation') : $t('move_to_locked_folder_confirmation'),
confirmText: $t('move'),
confirmColor: isLocked ? 'danger' : 'primary',
});
if (!isConfirmed) {
return;
}
try {
preAction({
type: isLocked ? AssetAction.SET_VISIBILITY_TIMELINE : AssetAction.SET_VISIBILITY_LOCKED,
asset,
});
await updateAssets({
assetBulkUpdateDto: {
ids: [asset.id],
visibility: isLocked ? AssetVisibility.Timeline : AssetVisibility.Locked,
},
});
onAction({
type: isLocked ? AssetAction.SET_VISIBILITY_TIMELINE : AssetAction.SET_VISIBILITY_LOCKED,
asset,
});
} catch (error) {
handleError(error, $t('errors.unable_to_save_settings'));
}
};
</script>
<MenuOption
onClick={() => toggleLockedVisibility()}
text={isLocked ? $t('move_off_locked_folder') : $t('add_to_locked_folder')}
icon={isLocked ? mdiFolderMoveOutline : mdiEyeOffOutline}
/>

View File

@@ -12,6 +12,7 @@
import SetAlbumCoverAction from '$lib/components/asset-viewer/actions/set-album-cover-action.svelte';
import SetFeaturedPhotoAction from '$lib/components/asset-viewer/actions/set-person-featured-action.svelte';
import SetProfilePictureAction from '$lib/components/asset-viewer/actions/set-profile-picture-action.svelte';
import SetVisibilityAction from '$lib/components/asset-viewer/actions/set-visibility-action.svelte';
import ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte';
import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-action.svelte';
import UnstackAction from '$lib/components/asset-viewer/actions/unstack-action.svelte';
@@ -27,6 +28,7 @@
import {
AssetJobName,
AssetTypeEnum,
Visibility,
type AlbumResponseDto,
type AssetResponseDto,
type PersonResponseDto,
@@ -91,6 +93,7 @@
const sharedLink = getSharedLink();
let isOwner = $derived($user && asset.ownerId === $user?.id);
let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline);
let isLocked = $derived(asset.visibility === Visibility.Locked);
// $: showEditorButton =
// isOwner &&
@@ -112,7 +115,7 @@
{/if}
</div>
<div class="flex gap-2 overflow-x-auto text-white" data-testid="asset-viewer-navbar-actions">
{#if !asset.isTrashed && $user}
{#if !asset.isTrashed && $user && !isLocked}
<ShareAction {asset} />
{/if}
{#if asset.isOffline}
@@ -159,17 +162,20 @@
<DeleteAction {asset} {onAction} {preAction} />
<ButtonContextMenu direction="left" align="top-right" color="opaque" title={$t('more')} icon={mdiDotsVertical}>
{#if showSlideshow}
{#if showSlideshow && !isLocked}
<MenuOption icon={mdiPresentationPlay} text={$t('slideshow')} onClick={onPlaySlideshow} />
{/if}
{#if showDownloadButton}
<DownloadAction {asset} menuItem />
{/if}
{#if asset.isTrashed}
<RestoreAction {asset} {onAction} />
{:else}
<AddToAlbumAction {asset} {onAction} />
<AddToAlbumAction {asset} {onAction} shared />
{#if !isLocked}
{#if asset.isTrashed}
<RestoreAction {asset} {onAction} />
{:else}
<AddToAlbumAction {asset} {onAction} />
<AddToAlbumAction {asset} {onAction} shared />
{/if}
{/if}
{#if isOwner}
@@ -183,21 +189,28 @@
{#if person}
<SetFeaturedPhotoAction {asset} {person} />
{/if}
{#if asset.type === AssetTypeEnum.Image}
{#if asset.type === AssetTypeEnum.Image && !isLocked}
<SetProfilePictureAction {asset} />
{/if}
<ArchiveAction {asset} {onAction} {preAction} />
<MenuOption
icon={mdiUpload}
onClick={() => openFileUploadDialog({ multiple: false, assetId: asset.id })}
text={$t('replace_with_upload')}
/>
{#if !asset.isArchived && !asset.isTrashed}
{#if !isLocked}
<ArchiveAction {asset} {onAction} {preAction} />
<MenuOption
icon={mdiImageSearch}
onClick={() => goto(`${AppRoute.PHOTOS}?at=${stack?.primaryAssetId ?? asset.id}`)}
text={$t('view_in_timeline')}
icon={mdiUpload}
onClick={() => openFileUploadDialog({ multiple: false, assetId: asset.id })}
text={$t('replace_with_upload')}
/>
{#if !asset.isArchived && !asset.isTrashed}
<MenuOption
icon={mdiImageSearch}
onClick={() => goto(`${AppRoute.PHOTOS}?at=${stack?.primaryAssetId ?? asset.id}`)}
text={$t('view_in_timeline')}
/>
{/if}
{/if}
{#if !asset.isTrashed}
<SetVisibilityAction {asset} {onAction} {preAction} />
{/if}
<hr />
<MenuOption