refactor: asset viewer navbar actions (#25091)

This commit is contained in:
Jason Rasmussen
2026-01-06 17:35:37 -05:00
committed by GitHub
parent f0f1687c79
commit 1a24a2d35e
8 changed files with 72 additions and 117 deletions

View File

@@ -8,7 +8,7 @@ import * as Oazapfts from "@oazapfts/runtime";
import * as QS from "@oazapfts/runtime/query";
export const defaults: Oazapfts.Defaults<Oazapfts.CustomHeaders> = {
headers: {},
baseUrl: "/api",
baseUrl: "/api"
};
const oazapfts = Oazapfts.runtime(defaults);
export const servers = {

View File

@@ -1,23 +0,0 @@
<script lang="ts">
import { shortcut } from '$lib/actions/shortcut';
import { IconButton } from '@immich/ui';
import { mdiArrowLeft } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
onClose: () => void;
}
let { onClose }: Props = $props();
</script>
<svelte:document use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onClose }} />
<IconButton
color="secondary"
variant="ghost"
shape="round"
icon={mdiArrowLeft}
aria-label={$t('go_back')}
onclick={onClose}
/>

View File

@@ -1,21 +0,0 @@
<script lang="ts">
import { IconButton } from '@immich/ui';
import { mdiMotionPauseOutline, mdiMotionPlayOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
isPlaying: boolean;
onClick: (shouldPlay: boolean) => void;
}
let { isPlaying, onClick }: Props = $props();
</script>
<IconButton
color="secondary"
variant="ghost"
shape="round"
icon={isPlaying ? mdiMotionPauseOutline : mdiMotionPlayOutline}
aria-label={isPlaying ? $t('stop_motion_photo') : $t('play_motion_photo')}
onclick={() => onClick(!isPlaying)}
/>

View File

@@ -1,22 +0,0 @@
<script lang="ts">
import { shortcut } from '$lib/actions/shortcut';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { IconButton } from '@immich/ui';
import { mdiInformationOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
const onAction = () => {
assetViewerManager.toggleDetailPanel();
};
</script>
<svelte:document use:shortcut={{ shortcut: { key: 'i' }, onShortcut: onAction }} />
<IconButton
color="secondary"
shape="round"
variant="ghost"
icon={mdiInformationOutline}
onclick={onAction}
aria-label={$t('info')}
/>

View File

@@ -7,7 +7,6 @@
import AddToAlbumAction from '$lib/components/asset-viewer/actions/add-to-album-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 CloseAction from '$lib/components/asset-viewer/actions/close-action.svelte';
import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte';
import DownloadAction from '$lib/components/asset-viewer/actions/download-action.svelte';
import FavoriteAction from '$lib/components/asset-viewer/actions/favorite-action.svelte';
@@ -20,12 +19,10 @@
import SetProfilePictureAction from '$lib/components/asset-viewer/actions/set-profile-picture-action.svelte';
import SetStackPrimaryAsset from '$lib/components/asset-viewer/actions/set-stack-primary-asset.svelte';
import SetVisibilityAction from '$lib/components/asset-viewer/actions/set-visibility-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';
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 { AppRoute } from '$lib/constants';
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { getAssetActions, handleReplaceAsset } from '$lib/services/asset.service';
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
@@ -44,9 +41,9 @@
type PersonResponseDto,
type StackResponseDto,
} from '@immich/sdk';
import { IconButton } from '@immich/ui';
import { CommandPaletteDefaultProvider, IconButton, type ActionItem } from '@immich/ui';
import {
mdiAlertOutline,
mdiArrowLeft,
mdiCogRefreshOutline,
mdiCompare,
mdiContentCopy,
@@ -61,7 +58,6 @@
mdiUpload,
mdiVideoOutline,
} from '@mdi/js';
import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n';
interface Props {
@@ -79,7 +75,6 @@
onPlaySlideshow: () => void;
// export let showEditorHandler: () => void;
onClose?: () => void;
motionPhoto?: Snippet;
playOriginalVideo: boolean;
setPlayOriginalVideo: (value: boolean) => void;
}
@@ -98,7 +93,6 @@
onRunJob,
onPlaySlideshow,
onClose,
motionPhoto,
playOriginalVideo = false,
setPlayOriginalVideo,
}: Props = $props();
@@ -109,7 +103,15 @@
let isLocked = $derived(asset.visibility === AssetVisibility.Locked);
let smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch);
const { Share } = $derived(getAssetActions($t, asset));
const Close: ActionItem = {
title: $t('go_back'),
icon: mdiArrowLeft,
$if: () => !!onClose,
onAction: () => onClose?.(),
shortcuts: [{ key: 'Escape' }],
};
const { Share, Offline, PlayMotionPhoto, StopMotionPhoto, Info } = $derived(getAssetActions($t, asset));
// $: showEditorButton =
// isOwner &&
@@ -122,30 +124,26 @@
// !asset.livePhotoVideoId;
</script>
<CommandPaletteDefaultProvider
name={$t('assets')}
actions={[Close, Share, Offline, PlayMotionPhoto, StopMotionPhoto, Info]}
/>
<div
class="flex h-16 place-items-center justify-between bg-linear-to-b from-black/40 px-3 transition-transform duration-200"
>
<div class="dark">
{#if onClose}
<CloseAction {onClose} />
{/if}
<ActionButton action={Close} />
</div>
<div class="flex gap-2 overflow-x-auto dark" data-testid="asset-viewer-navbar-actions">
<CastButton />
<ActionButton action={Share} />
{#if asset.isOffline}
<IconButton
shape="round"
color="danger"
icon={mdiAlertOutline}
onclick={() => assetViewerManager.toggleDetailPanel()}
aria-label={$t('asset_offline')}
/>
{/if}
{#if asset.livePhotoVideoId}
{@render motionPhoto?.()}
{/if}
<ActionButton action={Offline} />
<ActionButton action={PlayMotionPhoto} />
<ActionButton action={StopMotionPhoto} />
{#if asset.type === AssetTypeEnum.Image}
<IconButton
class="hidden sm:flex"
@@ -172,9 +170,7 @@
<DownloadAction asset={toTimelineAsset(asset)} />
{/if}
{#if asset.hasMetadata}
<ShowDetailAction />
{/if}
<ActionButton action={Info} />
{#if isOwner}
<FavoriteAction {asset} {onAction} />

View File

@@ -2,7 +2,6 @@
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';
@@ -102,7 +101,6 @@
const stackSelectedThumbnailSize = 65;
let appearsInAlbums: AlbumResponseDto[] = $state([]);
let shouldPlayMotionPhoto = $state(false);
let sharedLink = getSharedLink();
let previewStackedAsset: AssetResponseDto | undefined = $state();
let isShowEditor = $state(false);
@@ -420,14 +418,7 @@
onClose={onClose ? () => onClose(asset) : undefined}
{playOriginalVideo}
{setPlayOriginalVideo}
>
{#snippet motionPhoto()}
<MotionPhotoAction
isPlaying={shouldPlayMotionPhoto}
onClick={(shouldPlay) => (shouldPlayMotionPhoto = shouldPlay)}
/>
{/snippet}
</AssetViewerNavBar>
/>
</div>
{/if}
@@ -483,7 +474,7 @@
{:else}
{#key asset.id}
{#if asset.type === AssetTypeEnum.Image}
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
{#if assetViewerManager.isPlayingMotionPhoto && asset.livePhotoVideoId}
<VideoViewer
assetId={asset.livePhotoVideoId}
cacheKey={asset.thumbhash}
@@ -491,7 +482,7 @@
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
onPreviousAsset={() => navigateAsset('previous')}
onNextAsset={() => navigateAsset('next')}
onVideoEnded={() => (shouldPlayMotionPhoto = false)}
onVideoEnded={() => (assetViewerManager.isPlayingMotionPhoto = false)}
{playOriginalVideo}
/>
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath

View File

@@ -3,15 +3,8 @@ import { PersistedLocalStorage } from '$lib/utils/persisted';
const isShowDetailPanel = new PersistedLocalStorage<boolean>('asset-viewer-state', false);
export class AssetViewerManager {
#isShowActivityPanel = $state(false);
get isShowActivityPanel() {
return this.#isShowActivityPanel;
}
private set isShowActivityPanel(value: boolean) {
this.#isShowActivityPanel = value;
}
isShowActivityPanel = $state(false);
isPlayingMotionPhoto = $state(false);
get isShowDetailPanel() {
return isShowDetailPanel.current;

View File

@@ -1,10 +1,17 @@
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import { user as authUser } from '$lib/stores/user.store';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { AssetVisibility, copyAsset, deleteAssets, type AssetResponseDto } from '@immich/sdk';
import { modalManager, type ActionItem } from '@immich/ui';
import { mdiShareVariantOutline } from '@mdi/js';
import {
mdiAlertOutline,
mdiInformationOutline,
mdiMotionPauseOutline,
mdiMotionPlayOutline,
mdiShareVariantOutline,
} from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n';
import { get } from 'svelte/store';
@@ -16,7 +23,41 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
onAction: () => modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] }),
};
return { Share };
const PlayMotionPhoto: ActionItem = {
title: $t('play_motion_photo'),
icon: mdiMotionPlayOutline,
$if: () => !!asset.livePhotoVideoId && !assetViewerManager.isPlayingMotionPhoto,
onAction: () => {
assetViewerManager.isPlayingMotionPhoto = true;
},
};
const StopMotionPhoto: ActionItem = {
title: $t('stop_motion_photo'),
icon: mdiMotionPauseOutline,
$if: () => !!asset.livePhotoVideoId && assetViewerManager.isPlayingMotionPhoto,
onAction: () => {
assetViewerManager.isPlayingMotionPhoto = false;
},
};
const Offline: ActionItem = {
title: $t('asset_offline'),
icon: mdiAlertOutline,
color: 'danger',
$if: () => !!asset.isOffline,
onAction: () => assetViewerManager.toggleDetailPanel(),
};
const Info: ActionItem = {
title: $t('info'),
icon: mdiInformationOutline,
$if: () => asset.hasMetadata,
onAction: () => assetViewerManager.toggleDetailPanel(),
shortcuts: [{ key: 'i' }],
};
return { Share, PlayMotionPhoto, StopMotionPhoto, Offline, Info };
};
export const handleReplaceAsset = async (oldAssetId: string) => {