mirror of
https://github.com/immich-app/immich.git
synced 2026-03-26 20:00:44 +03:00
refactor: asset navbar (#25476)
This commit is contained in:
@@ -1,20 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { IconButton } from '@immich/ui';
|
|
||||||
import { mdiTune } from '@mdi/js';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onAction: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { onAction }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<IconButton
|
|
||||||
color="secondary"
|
|
||||||
shape="round"
|
|
||||||
variant="ghost"
|
|
||||||
icon={mdiTune}
|
|
||||||
aria-label={$t('editor')}
|
|
||||||
onclick={() => onAction()}
|
|
||||||
/>
|
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
import AddToStackAction from '$lib/components/asset-viewer/actions/add-to-stack-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 ArchiveAction from '$lib/components/asset-viewer/actions/archive-action.svelte';
|
||||||
import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte';
|
import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte';
|
||||||
import EditAction from '$lib/components/asset-viewer/actions/edit-action.svelte';
|
|
||||||
import KeepThisDeleteOthersAction from '$lib/components/asset-viewer/actions/keep-this-delete-others.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 RatingAction from '$lib/components/asset-viewer/actions/rating-action.svelte';
|
||||||
import RemoveAssetFromStack from '$lib/components/asset-viewer/actions/remove-asset-from-stack.svelte';
|
import RemoveAssetFromStack from '$lib/components/asset-viewer/actions/remove-asset-from-stack.svelte';
|
||||||
@@ -20,7 +19,6 @@
|
|||||||
import UnstackAction from '$lib/components/asset-viewer/actions/unstack-action.svelte';
|
import UnstackAction from '$lib/components/asset-viewer/actions/unstack-action.svelte';
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
import { ProjectionType } from '$lib/constants';
|
|
||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
import { Route } from '$lib/route';
|
import { Route } from '$lib/route';
|
||||||
import { getGlobalActions } from '$lib/services/app.service';
|
import { getGlobalActions } from '$lib/services/app.service';
|
||||||
@@ -67,7 +65,6 @@
|
|||||||
onAction: OnAction;
|
onAction: OnAction;
|
||||||
onUndoDelete?: OnUndoDelete;
|
onUndoDelete?: OnUndoDelete;
|
||||||
onPlaySlideshow: () => void;
|
onPlaySlideshow: () => void;
|
||||||
onEdit: () => void;
|
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
playOriginalVideo: boolean;
|
playOriginalVideo: boolean;
|
||||||
setPlayOriginalVideo: (value: boolean) => void;
|
setPlayOriginalVideo: (value: boolean) => void;
|
||||||
@@ -86,25 +83,41 @@
|
|||||||
onUndoDelete = undefined,
|
onUndoDelete = undefined,
|
||||||
onPlaySlideshow,
|
onPlaySlideshow,
|
||||||
onClose,
|
onClose,
|
||||||
onEdit,
|
|
||||||
playOriginalVideo = false,
|
playOriginalVideo = false,
|
||||||
setPlayOriginalVideo,
|
setPlayOriginalVideo,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let isOwner = $derived($user && asset.ownerId === $user?.id);
|
const isOwner = $derived($user && asset.ownerId === $user?.id);
|
||||||
let isLocked = $derived(asset.visibility === AssetVisibility.Locked);
|
const isLocked = $derived(asset.visibility === AssetVisibility.Locked);
|
||||||
let smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch);
|
const isImage = $derived(asset.type === AssetTypeEnum.Image);
|
||||||
|
const smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch);
|
||||||
|
|
||||||
const Close: ActionItem = {
|
const { Cast } = $derived(getGlobalActions($t));
|
||||||
|
|
||||||
|
const { Close, ZoomIn, ZoomOut } = $derived({
|
||||||
|
Close: {
|
||||||
title: $t('go_back'),
|
title: $t('go_back'),
|
||||||
type: $t('assets'),
|
type: $t('assets'),
|
||||||
icon: mdiArrowLeft,
|
icon: mdiArrowLeft,
|
||||||
$if: () => !!onClose,
|
$if: () => !!onClose,
|
||||||
onAction: () => onClose?.(),
|
onAction: () => onClose?.(),
|
||||||
shortcuts: [{ key: 'Escape' }],
|
shortcuts: [{ key: 'Escape' }],
|
||||||
};
|
},
|
||||||
|
|
||||||
const { Cast } = $derived(getGlobalActions($t));
|
ZoomIn: {
|
||||||
|
title: $t('zoom_image'),
|
||||||
|
icon: mdiMagnifyPlusOutline,
|
||||||
|
$if: () => isImage && $photoZoomState && $photoZoomState.currentZoom <= 1,
|
||||||
|
onAction: () => onZoomImage(),
|
||||||
|
},
|
||||||
|
|
||||||
|
ZoomOut: {
|
||||||
|
title: $t('zoom_image'),
|
||||||
|
icon: mdiMagnifyMinusOutline,
|
||||||
|
$if: () => $photoZoomState && $photoZoomState.currentZoom > 1,
|
||||||
|
onAction: () => onZoomImage(),
|
||||||
|
},
|
||||||
|
} satisfies Record<string, ActionItem>);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
Share,
|
Share,
|
||||||
@@ -117,22 +130,13 @@
|
|||||||
PlayMotionPhoto,
|
PlayMotionPhoto,
|
||||||
StopMotionPhoto,
|
StopMotionPhoto,
|
||||||
Info,
|
Info,
|
||||||
|
Edit,
|
||||||
RefreshFacesJob,
|
RefreshFacesJob,
|
||||||
RefreshMetadataJob,
|
RefreshMetadataJob,
|
||||||
RegenerateThumbnailJob,
|
RegenerateThumbnailJob,
|
||||||
TranscodeVideoJob,
|
TranscodeVideoJob,
|
||||||
} = $derived(getAssetActions($t, asset));
|
} = $derived(getAssetActions($t, asset));
|
||||||
const sharedLink = getSharedLink();
|
const sharedLink = getSharedLink();
|
||||||
|
|
||||||
const editorDisabled = $derived(
|
|
||||||
!isOwner ||
|
|
||||||
asset.type !== AssetTypeEnum.Image ||
|
|
||||||
asset.livePhotoVideoId ||
|
|
||||||
(asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR &&
|
|
||||||
asset.originalPath.toLowerCase().endsWith('.insp')) ||
|
|
||||||
asset.originalPath.toLowerCase().endsWith('.gif') ||
|
|
||||||
asset.originalPath.toLowerCase().endsWith('.svg'),
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CommandPaletteDefaultProvider
|
<CommandPaletteDefaultProvider
|
||||||
@@ -150,6 +154,7 @@
|
|||||||
PlayMotionPhoto,
|
PlayMotionPhoto,
|
||||||
StopMotionPhoto,
|
StopMotionPhoto,
|
||||||
Info,
|
Info,
|
||||||
|
Edit,
|
||||||
RefreshFacesJob,
|
RefreshFacesJob,
|
||||||
RefreshMetadataJob,
|
RefreshMetadataJob,
|
||||||
RegenerateThumbnailJob,
|
RegenerateThumbnailJob,
|
||||||
@@ -170,18 +175,9 @@
|
|||||||
<ActionButton action={Offline} />
|
<ActionButton action={Offline} />
|
||||||
<ActionButton action={PlayMotionPhoto} />
|
<ActionButton action={PlayMotionPhoto} />
|
||||||
<ActionButton action={StopMotionPhoto} />
|
<ActionButton action={StopMotionPhoto} />
|
||||||
|
<ActionButton action={ZoomIn} />
|
||||||
|
<ActionButton action={ZoomOut} />
|
||||||
|
|
||||||
{#if asset.type === AssetTypeEnum.Image}
|
|
||||||
<IconButton
|
|
||||||
class="hidden sm:flex"
|
|
||||||
color="secondary"
|
|
||||||
variant="ghost"
|
|
||||||
shape="round"
|
|
||||||
icon={$photoZoomState && $photoZoomState.currentZoom > 1 ? mdiMagnifyMinusOutline : mdiMagnifyPlusOutline}
|
|
||||||
aria-label={$t('zoom_image')}
|
|
||||||
onclick={onZoomImage}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
{#if canCopyImageToClipboard() && asset.type === AssetTypeEnum.Image && $photoViewerImgElement}
|
{#if canCopyImageToClipboard() && asset.type === AssetTypeEnum.Image && $photoViewerImgElement}
|
||||||
<IconButton
|
<IconButton
|
||||||
color="secondary"
|
color="secondary"
|
||||||
@@ -202,9 +198,7 @@
|
|||||||
<RatingAction {asset} {onAction} />
|
<RatingAction {asset} {onAction} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !editorDisabled}
|
<ActionButton action={Edit} />
|
||||||
<EditAction onAction={onEdit} />
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<DeleteAction {asset} {onAction} {preAction} {onUndoDelete} />
|
<DeleteAction {asset} {onAction} {preAction} {onUndoDelete} />
|
||||||
|
|||||||
@@ -106,7 +106,6 @@
|
|||||||
let appearsInAlbums: AlbumResponseDto[] = $state([]);
|
let appearsInAlbums: AlbumResponseDto[] = $state([]);
|
||||||
let sharedLink = getSharedLink();
|
let sharedLink = getSharedLink();
|
||||||
let previewStackedAsset: AssetResponseDto | undefined = $state();
|
let previewStackedAsset: AssetResponseDto | undefined = $state();
|
||||||
let isShowEditor = $state(false);
|
|
||||||
let fullscreenElement = $state<Element>();
|
let fullscreenElement = $state<Element>();
|
||||||
let unsubscribes: (() => void)[] = [];
|
let unsubscribes: (() => void)[] = [];
|
||||||
let stack: StackResponseDto | null = $state(null);
|
let stack: StackResponseDto | null = $state(null);
|
||||||
@@ -202,7 +201,7 @@
|
|||||||
onAssetChange?.(refreshedAsset);
|
onAssetChange?.(refreshedAsset);
|
||||||
assetViewingStore.setAsset(refreshedAsset);
|
assetViewingStore.setAsset(refreshedAsset);
|
||||||
}
|
}
|
||||||
isShowEditor = false;
|
assetViewerManager.closeEditor();
|
||||||
};
|
};
|
||||||
|
|
||||||
const tracker = new InvocationTracker();
|
const tracker = new InvocationTracker();
|
||||||
@@ -249,13 +248,6 @@
|
|||||||
}, $t('error_while_navigating'));
|
}, $t('error_while_navigating'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const showEditor = () => {
|
|
||||||
if (assetViewerManager.isShowActivityPanel) {
|
|
||||||
assetViewerManager.isShowActivityPanel = false;
|
|
||||||
}
|
|
||||||
isShowEditor = !isShowEditor;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Slide show mode
|
* Slide show mode
|
||||||
*/
|
*/
|
||||||
@@ -412,7 +404,7 @@
|
|||||||
) {
|
) {
|
||||||
return 'ImagePanaramaViewer';
|
return 'ImagePanaramaViewer';
|
||||||
}
|
}
|
||||||
if (isShowEditor && editManager.selectedTool?.type === EditToolType.Transform) {
|
if (assetViewerManager.isShowEditor && editManager.selectedTool?.type === EditToolType.Transform) {
|
||||||
return 'CropArea';
|
return 'CropArea';
|
||||||
}
|
}
|
||||||
return 'PhotoViewer';
|
return 'PhotoViewer';
|
||||||
@@ -429,7 +421,7 @@
|
|||||||
$slideshowState === SlideshowState.None &&
|
$slideshowState === SlideshowState.None &&
|
||||||
asset.type === AssetTypeEnum.Image &&
|
asset.type === AssetTypeEnum.Image &&
|
||||||
!(asset.exifInfo?.projectionType === 'EQUIRECTANGULAR') &&
|
!(asset.exifInfo?.projectionType === 'EQUIRECTANGULAR') &&
|
||||||
!isShowEditor &&
|
!assetViewerManager.isShowEditor &&
|
||||||
ocrManager.hasOcrData,
|
ocrManager.hasOcrData,
|
||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
@@ -445,7 +437,7 @@
|
|||||||
bind:this={assetViewerHtmlElement}
|
bind:this={assetViewerHtmlElement}
|
||||||
>
|
>
|
||||||
<!-- Top navigation bar -->
|
<!-- Top navigation bar -->
|
||||||
{#if $slideshowState === SlideshowState.None && !isShowEditor}
|
{#if $slideshowState === SlideshowState.None && !assetViewerManager.isShowEditor}
|
||||||
<div class="col-span-4 col-start-1 row-span-1 row-start-1 transition-transform">
|
<div class="col-span-4 col-start-1 row-span-1 row-start-1 transition-transform">
|
||||||
<AssetViewerNavBar
|
<AssetViewerNavBar
|
||||||
{asset}
|
{asset}
|
||||||
@@ -458,7 +450,6 @@
|
|||||||
preAction={handlePreAction}
|
preAction={handlePreAction}
|
||||||
onAction={handleAction}
|
onAction={handleAction}
|
||||||
{onUndoDelete}
|
{onUndoDelete}
|
||||||
onEdit={showEditor}
|
|
||||||
onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)}
|
onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)}
|
||||||
onClose={onClose ? () => onClose(asset) : undefined}
|
onClose={onClose ? () => onClose(asset) : undefined}
|
||||||
{playOriginalVideo}
|
{playOriginalVideo}
|
||||||
@@ -480,7 +471,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $slideshowState === SlideshowState.None && showNavigation && !isShowEditor && previousAsset}
|
{#if $slideshowState === SlideshowState.None && showNavigation && !assetViewerManager.isShowEditor && previousAsset}
|
||||||
<div class="my-auto col-span-1 col-start-1 row-span-full row-start-1 justify-self-start">
|
<div class="my-auto col-span-1 col-start-1 row-span-full row-start-1 justify-self-start">
|
||||||
<PreviousAssetAction onPreviousAsset={() => navigateAsset('previous')} />
|
<PreviousAssetAction onPreviousAsset={() => navigateAsset('previous')} />
|
||||||
</div>
|
</div>
|
||||||
@@ -571,13 +562,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $slideshowState === SlideshowState.None && showNavigation && !isShowEditor && nextAsset}
|
{#if $slideshowState === SlideshowState.None && showNavigation && !assetViewerManager.isShowEditor && nextAsset}
|
||||||
<div class="my-auto col-span-1 col-start-4 row-span-full row-start-1 justify-self-end">
|
<div class="my-auto col-span-1 col-start-4 row-span-full row-start-1 justify-self-end">
|
||||||
<NextAssetAction onNextAsset={() => navigateAsset('next')} />
|
<NextAssetAction onNextAsset={() => navigateAsset('next')} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if asset.hasMetadata && $slideshowState === SlideshowState.None && assetViewerManager.isShowDetailPanel && !isShowEditor}
|
{#if asset.hasMetadata && $slideshowState === SlideshowState.None && assetViewerManager.isShowDetailPanel && !assetViewerManager.isShowEditor}
|
||||||
<div
|
<div
|
||||||
transition:fly={{ duration: 150 }}
|
transition:fly={{ duration: 150 }}
|
||||||
id="detail-panel"
|
id="detail-panel"
|
||||||
@@ -588,7 +579,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isShowEditor}
|
{#if assetViewerManager.isShowEditor}
|
||||||
<div
|
<div
|
||||||
transition:fly={{ duration: 150 }}
|
transition:fly={{ duration: 150 }}
|
||||||
id="editor-panel"
|
id="editor-panel"
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const isShowDetailPanel = new PersistedLocalStorage<boolean>('asset-viewer-state
|
|||||||
export class AssetViewerManager {
|
export class AssetViewerManager {
|
||||||
isShowActivityPanel = $state(false);
|
isShowActivityPanel = $state(false);
|
||||||
isPlayingMotionPhoto = $state(false);
|
isPlayingMotionPhoto = $state(false);
|
||||||
|
isShowEditor = $state(false);
|
||||||
|
|
||||||
get isShowDetailPanel() {
|
get isShowDetailPanel() {
|
||||||
return isShowDetailPanel.current;
|
return isShowDetailPanel.current;
|
||||||
@@ -31,6 +32,15 @@ export class AssetViewerManager {
|
|||||||
closeDetailPanel() {
|
closeDetailPanel() {
|
||||||
this.isShowDetailPanel = false;
|
this.isShowDetailPanel = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openEditor() {
|
||||||
|
this.closeActivityPanel();
|
||||||
|
this.isShowEditor = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
closeEditor() {
|
||||||
|
this.isShowEditor = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const assetViewerManager = new AssetViewerManager();
|
export const assetViewerManager = new AssetViewerManager();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ProjectionType } from '$lib/constants';
|
||||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
@@ -37,6 +38,7 @@ import {
|
|||||||
mdiMotionPauseOutline,
|
mdiMotionPauseOutline,
|
||||||
mdiMotionPlayOutline,
|
mdiMotionPlayOutline,
|
||||||
mdiShareVariantOutline,
|
mdiShareVariantOutline,
|
||||||
|
mdiTune,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import type { MessageFormatter } from 'svelte-i18n';
|
import type { MessageFormatter } from 'svelte-i18n';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
@@ -132,6 +134,21 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
|||||||
shortcuts: [{ key: 'i' }],
|
shortcuts: [{ key: 'i' }],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Edit: ActionItem = {
|
||||||
|
title: $t('editor'),
|
||||||
|
icon: mdiTune,
|
||||||
|
$if: () =>
|
||||||
|
!sharedLink &&
|
||||||
|
isOwner &&
|
||||||
|
asset.type === AssetTypeEnum.Image &&
|
||||||
|
!asset.livePhotoVideoId &&
|
||||||
|
asset.exifInfo?.projectionType !== ProjectionType.EQUIRECTANGULAR &&
|
||||||
|
!asset.originalPath.toLowerCase().endsWith('.insp') &&
|
||||||
|
!asset.originalPath.toLowerCase().endsWith('.gif') &&
|
||||||
|
!asset.originalPath.toLowerCase().endsWith('.svg'),
|
||||||
|
onAction: () => assetViewerManager.openEditor(),
|
||||||
|
};
|
||||||
|
|
||||||
const RefreshFacesJob: ActionItem = {
|
const RefreshFacesJob: ActionItem = {
|
||||||
title: getAssetJobName($t, AssetJobName.RefreshFaces),
|
title: getAssetJobName($t, AssetJobName.RefreshFaces),
|
||||||
icon: mdiHeadSyncOutline,
|
icon: mdiHeadSyncOutline,
|
||||||
@@ -168,6 +185,7 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
|||||||
Unfavorite,
|
Unfavorite,
|
||||||
PlayMotionPhoto,
|
PlayMotionPhoto,
|
||||||
StopMotionPhoto,
|
StopMotionPhoto,
|
||||||
|
Edit,
|
||||||
RefreshFacesJob,
|
RefreshFacesJob,
|
||||||
RefreshMetadataJob,
|
RefreshMetadataJob,
|
||||||
RegenerateThumbnailJob,
|
RegenerateThumbnailJob,
|
||||||
|
|||||||
Reference in New Issue
Block a user