mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
chore(web): context menu improvements (#10475)
- ability to add custom hover colors - migrate activity menu to ButtonContextMenu component - onClick callbacks rather than events for menu options - remove slots - configurable menu option colors - improve menu option layout
This commit is contained in:
parent
5cde52eec9
commit
0fda67543d
19 changed files with 102 additions and 125 deletions
|
|
@ -4,7 +4,6 @@
|
|||
import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||
import { getAssetType } from '$lib/utils/asset-utils';
|
||||
import { autoGrowHeight } from '$lib/actions/autogrow';
|
||||
import { clickOutside } from '$lib/actions/click-outside';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { isTenMinutesApart } from '$lib/utils/timesince';
|
||||
import {
|
||||
|
|
@ -16,7 +15,7 @@
|
|||
type AssetTypeEnum,
|
||||
type UserResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { mdiClose, mdiDotsVertical, mdiHeart, mdiSend } from '@mdi/js';
|
||||
import { mdiClose, mdiDotsVertical, mdiHeart, mdiSend, mdiDeleteOutline } from '@mdi/js';
|
||||
import * as luxon from 'luxon';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
|
|
@ -26,6 +25,8 @@
|
|||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import { t } from 'svelte-i18n';
|
||||
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';
|
||||
|
||||
const units: Intl.RelativeTimeFormatUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second'];
|
||||
|
||||
|
|
@ -71,7 +72,6 @@
|
|||
close: void;
|
||||
}>();
|
||||
|
||||
$: showDeleteReaction = Array.from({ length: reactions.length }).fill(false);
|
||||
$: {
|
||||
if (innerHeight && activityHeight) {
|
||||
divHeight = innerHeight - activityHeight;
|
||||
|
|
@ -109,7 +109,6 @@
|
|||
try {
|
||||
await deleteActivity({ id: reaction.id });
|
||||
reactions.splice(index, 1);
|
||||
showDeleteReaction.splice(index, 1);
|
||||
reactions = reactions;
|
||||
if (isLiked && reaction.type === 'like' && reaction.id == isLiked.id) {
|
||||
dispatch('deleteLike');
|
||||
|
|
@ -147,10 +146,6 @@
|
|||
}
|
||||
isSendingMessage = false;
|
||||
};
|
||||
|
||||
const showOptionsMenu = (index: number) => {
|
||||
showDeleteReaction[index] = !showDeleteReaction[index];
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="overflow-y-hidden relative h-full" bind:offsetHeight={innerHeight}>
|
||||
|
|
@ -188,27 +183,23 @@
|
|||
</a>
|
||||
{/if}
|
||||
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
||||
<div class="flex items-start w-fit pt-[5px]">
|
||||
<CircleIconButton
|
||||
<div class="mr-4">
|
||||
<ButtonContextMenu
|
||||
icon={mdiDotsVertical}
|
||||
title={$t('comment_options')}
|
||||
align="top-right"
|
||||
direction="left"
|
||||
size="16"
|
||||
on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))}
|
||||
/>
|
||||
>
|
||||
<MenuOption
|
||||
activeColor="bg-red-200"
|
||||
icon={mdiDeleteOutline}
|
||||
text={$t('remove')}
|
||||
onClick={() => handleDeleteReaction(reaction, index)}
|
||||
/>
|
||||
</ButtonContextMenu>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if showDeleteReaction[index]}
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-6 rounded-xl items-center bg-gray-300 dark:bg-slate-100 py-3 px-6 text-left text-sm font-medium text-immich-fg hover:bg-red-300 focus:outline-none focus:ring-2 focus:ring-inset dark:text-immich-dark-bg dark:hover:bg-red-100 transition-colors"
|
||||
use:clickOutside={{ onOutclick: () => (showDeleteReaction[index] = false) }}
|
||||
on:click={() => handleDeleteReaction(reaction, index)}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if (index != reactions.length - 1 && !shouldGroup(reactions[index].createdAt, reactions[index + 1].createdAt)) || index === reactions.length - 1}
|
||||
|
|
@ -240,27 +231,23 @@
|
|||
</a>
|
||||
{/if}
|
||||
{#if reaction.user.id === user.id || albumOwnerId === user.id}
|
||||
<div class="flex items-start w-fit">
|
||||
<CircleIconButton
|
||||
<div class="mr-4">
|
||||
<ButtonContextMenu
|
||||
icon={mdiDotsVertical}
|
||||
title={$t('reaction_options')}
|
||||
align="top-right"
|
||||
direction="left"
|
||||
size="16"
|
||||
on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))}
|
||||
/>
|
||||
>
|
||||
<MenuOption
|
||||
activeColor="bg-red-200"
|
||||
icon={mdiDeleteOutline}
|
||||
text={$t('remove')}
|
||||
onClick={() => handleDeleteReaction(reaction, index)}
|
||||
/>
|
||||
</ButtonContextMenu>
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
{#if showDeleteReaction[index]}
|
||||
<button
|
||||
type="button"
|
||||
class="absolute right-6 rounded-xl items-center bg-gray-300 dark:bg-slate-100 py-3 px-6 text-left text-sm font-medium text-immich-fg hover:bg-red-300 focus:outline-none focus:ring-2 focus:ring-inset dark:text-immich-dark-bg dark:hover:bg-red-100 transition-colors"
|
||||
use:clickOutside={{ onOutclick: () => (showDeleteReaction[index] = false) }}
|
||||
on:click={() => handleDeleteReaction(reaction, index)}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if (index != reactions.length - 1 && isTenMinutesApart(reactions[index].createdAt, reactions[index + 1].createdAt)) || index === reactions.length - 1}
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -177,65 +177,65 @@
|
|||
/>
|
||||
<ButtonContextMenu direction="left" align="top-right" color="opaque" title={$t('more')} icon={mdiDotsVertical}>
|
||||
{#if showSlideshow}
|
||||
<MenuOption icon={mdiPresentationPlay} on:click={() => onMenuClick('playSlideShow')} text={$t('slideshow')} />
|
||||
<MenuOption icon={mdiPresentationPlay} onClick={() => onMenuClick('playSlideShow')} text={$t('slideshow')} />
|
||||
{/if}
|
||||
{#if showDownloadButton}
|
||||
<MenuOption icon={mdiFolderDownloadOutline} on:click={() => onMenuClick('download')} text={$t('download')} />
|
||||
<MenuOption icon={mdiFolderDownloadOutline} onClick={() => onMenuClick('download')} text={$t('download')} />
|
||||
{/if}
|
||||
{#if asset.isTrashed}
|
||||
<MenuOption icon={mdiHistory} on:click={() => onMenuClick('restoreAsset')} text={$t('restore')} />
|
||||
<MenuOption icon={mdiHistory} onClick={() => onMenuClick('restoreAsset')} text={$t('restore')} />
|
||||
{:else}
|
||||
<MenuOption icon={mdiImageAlbum} on:click={() => onMenuClick('addToAlbum')} text={$t('add_to_album')} />
|
||||
<MenuOption icon={mdiImageAlbum} onClick={() => onMenuClick('addToAlbum')} text={$t('add_to_album')} />
|
||||
<MenuOption
|
||||
icon={mdiShareVariantOutline}
|
||||
on:click={() => onMenuClick('addToSharedAlbum')}
|
||||
onClick={() => onMenuClick('addToSharedAlbum')}
|
||||
text={$t('add_to_shared_album')}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isOwner}
|
||||
{#if hasStackChildren}
|
||||
<MenuOption icon={mdiImageMinusOutline} on:click={() => onMenuClick('unstack')} text={$t('unstack')} />
|
||||
<MenuOption icon={mdiImageMinusOutline} onClick={() => onMenuClick('unstack')} text={$t('unstack')} />
|
||||
{/if}
|
||||
{#if album}
|
||||
<MenuOption
|
||||
text={$t('set_as_album_cover')}
|
||||
icon={mdiImageOutline}
|
||||
on:click={() => onMenuClick('setAsAlbumCover')}
|
||||
onClick={() => onMenuClick('setAsAlbumCover')}
|
||||
/>
|
||||
{/if}
|
||||
{#if asset.type === AssetTypeEnum.Image}
|
||||
<MenuOption
|
||||
icon={mdiAccountCircleOutline}
|
||||
on:click={() => onMenuClick('asProfileImage')}
|
||||
onClick={() => onMenuClick('asProfileImage')}
|
||||
text={$t('set_as_profile_picture')}
|
||||
/>
|
||||
{/if}
|
||||
<MenuOption
|
||||
on:click={() => onMenuClick('toggleArchive')}
|
||||
onClick={() => onMenuClick('toggleArchive')}
|
||||
icon={asset.isArchived ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline}
|
||||
text={asset.isArchived ? $t('unarchive') : $t('to_archive')}
|
||||
/>
|
||||
<MenuOption
|
||||
icon={mdiUpload}
|
||||
on:click={() => openFileUploadDialog({ multiple: false, assetId: asset.id })}
|
||||
onClick={() => openFileUploadDialog({ multiple: false, assetId: asset.id })}
|
||||
text={$t('replace_with_upload')}
|
||||
/>
|
||||
<hr />
|
||||
<MenuOption
|
||||
icon={mdiDatabaseRefreshOutline}
|
||||
on:click={() => onJobClick(AssetJobName.RefreshMetadata)}
|
||||
onClick={() => onJobClick(AssetJobName.RefreshMetadata)}
|
||||
text={getAssetJobName(AssetJobName.RefreshMetadata)}
|
||||
/>
|
||||
<MenuOption
|
||||
icon={mdiImageRefreshOutline}
|
||||
on:click={() => onJobClick(AssetJobName.RegenerateThumbnail)}
|
||||
onClick={() => onJobClick(AssetJobName.RegenerateThumbnail)}
|
||||
text={getAssetJobName(AssetJobName.RegenerateThumbnail)}
|
||||
/>
|
||||
{#if asset.type === AssetTypeEnum.Video}
|
||||
<MenuOption
|
||||
icon={mdiCogRefreshOutline}
|
||||
on:click={() => onJobClick(AssetJobName.TranscodeVideo)}
|
||||
onClick={() => onJobClick(AssetJobName.TranscodeVideo)}
|
||||
text={getAssetJobName(AssetJobName.TranscodeVideo)}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue