You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02: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:
		| @@ -424,13 +424,13 @@ | |||||||
|     <MenuOption |     <MenuOption | ||||||
|       icon={mdiRenameOutline} |       icon={mdiRenameOutline} | ||||||
|       text={$t('edit_album')} |       text={$t('edit_album')} | ||||||
|       on:click={() => contextMenuTargetAlbum && handleEdit(contextMenuTargetAlbum)} |       onClick={() => contextMenuTargetAlbum && handleEdit(contextMenuTargetAlbum)} | ||||||
|     /> |     /> | ||||||
|     <MenuOption icon={mdiShareVariantOutline} text={$t('share')} on:click={() => openShareModal()} /> |     <MenuOption icon={mdiShareVariantOutline} text={$t('share')} onClick={() => openShareModal()} /> | ||||||
|   {/if} |   {/if} | ||||||
|   <MenuOption icon={mdiFolderDownloadOutline} text={$t('download')} on:click={() => handleDownloadAlbum()} /> |   <MenuOption icon={mdiFolderDownloadOutline} text={$t('download')} onClick={() => handleDownloadAlbum()} /> | ||||||
|   {#if showFullContextMenu} |   {#if showFullContextMenu} | ||||||
|     <MenuOption icon={mdiDeleteOutline} text={$t('delete')} on:click={() => setAlbumToDelete()} /> |     <MenuOption icon={mdiDeleteOutline} text={$t('delete')} onClick={() => setAlbumToDelete()} /> | ||||||
|   {/if} |   {/if} | ||||||
| </RightClickContextMenu> | </RightClickContextMenu> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -109,14 +109,14 @@ | |||||||
|             {#if isOwned} |             {#if isOwned} | ||||||
|               <ButtonContextMenu icon={mdiDotsVertical} size="20" title={$t('options')}> |               <ButtonContextMenu icon={mdiDotsVertical} size="20" title={$t('options')}> | ||||||
|                 {#if role === AlbumUserRole.Viewer} |                 {#if role === AlbumUserRole.Viewer} | ||||||
|                   <MenuOption on:click={() => handleSetReadonly(user, AlbumUserRole.Editor)} text={$t('allow_edits')} /> |                   <MenuOption onClick={() => handleSetReadonly(user, AlbumUserRole.Editor)} text={$t('allow_edits')} /> | ||||||
|                 {:else} |                 {:else} | ||||||
|                   <MenuOption |                   <MenuOption | ||||||
|                     on:click={() => handleSetReadonly(user, AlbumUserRole.Viewer)} |                     onClick={() => handleSetReadonly(user, AlbumUserRole.Viewer)} | ||||||
|                     text={$t('disallow_edits')} |                     text={$t('disallow_edits')} | ||||||
|                   /> |                   /> | ||||||
|                 {/if} |                 {/if} | ||||||
|                 <MenuOption on:click={() => handleMenuRemove(user)} text={$t('remove')} /> |                 <MenuOption onClick={() => handleMenuRemove(user)} text={$t('remove')} /> | ||||||
|               </ButtonContextMenu> |               </ButtonContextMenu> | ||||||
|             {:else if user.id == currentUser?.id} |             {:else if user.id == currentUser?.id} | ||||||
|               <button |               <button | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ | |||||||
|   import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils'; |   import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils'; | ||||||
|   import { getAssetType } from '$lib/utils/asset-utils'; |   import { getAssetType } from '$lib/utils/asset-utils'; | ||||||
|   import { autoGrowHeight } from '$lib/actions/autogrow'; |   import { autoGrowHeight } from '$lib/actions/autogrow'; | ||||||
|   import { clickOutside } from '$lib/actions/click-outside'; |  | ||||||
|   import { handleError } from '$lib/utils/handle-error'; |   import { handleError } from '$lib/utils/handle-error'; | ||||||
|   import { isTenMinutesApart } from '$lib/utils/timesince'; |   import { isTenMinutesApart } from '$lib/utils/timesince'; | ||||||
|   import { |   import { | ||||||
| @@ -16,7 +15,7 @@ | |||||||
|     type AssetTypeEnum, |     type AssetTypeEnum, | ||||||
|     type UserResponseDto, |     type UserResponseDto, | ||||||
|   } from '@immich/sdk'; |   } 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 * as luxon from 'luxon'; | ||||||
|   import { createEventDispatcher, onMount } from 'svelte'; |   import { createEventDispatcher, onMount } from 'svelte'; | ||||||
|   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; |   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; | ||||||
| @@ -26,6 +25,8 @@ | |||||||
|   import { locale } from '$lib/stores/preferences.store'; |   import { locale } from '$lib/stores/preferences.store'; | ||||||
|   import { shortcut } from '$lib/actions/shortcut'; |   import { shortcut } from '$lib/actions/shortcut'; | ||||||
|   import { t } from 'svelte-i18n'; |   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']; |   const units: Intl.RelativeTimeFormatUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second']; | ||||||
|  |  | ||||||
| @@ -71,7 +72,6 @@ | |||||||
|     close: void; |     close: void; | ||||||
|   }>(); |   }>(); | ||||||
|  |  | ||||||
|   $: showDeleteReaction = Array.from({ length: reactions.length }).fill(false); |  | ||||||
|   $: { |   $: { | ||||||
|     if (innerHeight && activityHeight) { |     if (innerHeight && activityHeight) { | ||||||
|       divHeight = innerHeight - activityHeight; |       divHeight = innerHeight - activityHeight; | ||||||
| @@ -109,7 +109,6 @@ | |||||||
|     try { |     try { | ||||||
|       await deleteActivity({ id: reaction.id }); |       await deleteActivity({ id: reaction.id }); | ||||||
|       reactions.splice(index, 1); |       reactions.splice(index, 1); | ||||||
|       showDeleteReaction.splice(index, 1); |  | ||||||
|       reactions = reactions; |       reactions = reactions; | ||||||
|       if (isLiked && reaction.type === 'like' && reaction.id == isLiked.id) { |       if (isLiked && reaction.type === 'like' && reaction.id == isLiked.id) { | ||||||
|         dispatch('deleteLike'); |         dispatch('deleteLike'); | ||||||
| @@ -147,10 +146,6 @@ | |||||||
|     } |     } | ||||||
|     isSendingMessage = false; |     isSendingMessage = false; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const showOptionsMenu = (index: number) => { |  | ||||||
|     showDeleteReaction[index] = !showDeleteReaction[index]; |  | ||||||
|   }; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <div class="overflow-y-hidden relative h-full" bind:offsetHeight={innerHeight}> | <div class="overflow-y-hidden relative h-full" bind:offsetHeight={innerHeight}> | ||||||
| @@ -188,27 +183,23 @@ | |||||||
|                 </a> |                 </a> | ||||||
|               {/if} |               {/if} | ||||||
|               {#if reaction.user.id === user.id || albumOwnerId === user.id} |               {#if reaction.user.id === user.id || albumOwnerId === user.id} | ||||||
|                 <div class="flex items-start w-fit pt-[5px]"> |                 <div class="mr-4"> | ||||||
|                   <CircleIconButton |                   <ButtonContextMenu | ||||||
|                     icon={mdiDotsVertical} |                     icon={mdiDotsVertical} | ||||||
|                     title={$t('comment_options')} |                     title={$t('comment_options')} | ||||||
|  |                     align="top-right" | ||||||
|  |                     direction="left" | ||||||
|                     size="16" |                     size="16" | ||||||
|                     on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))} |  | ||||||
|                   /> |  | ||||||
|                 </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 |                     <MenuOption | ||||||
|                   </button> |                       activeColor="bg-red-200" | ||||||
|                 {/if} |                       icon={mdiDeleteOutline} | ||||||
|  |                       text={$t('remove')} | ||||||
|  |                       onClick={() => handleDeleteReaction(reaction, index)} | ||||||
|  |                     /> | ||||||
|  |                   </ButtonContextMenu> | ||||||
|                 </div> |                 </div> | ||||||
|  |               {/if} | ||||||
|             </div> |             </div> | ||||||
|  |  | ||||||
|             {#if (index != reactions.length - 1 && !shouldGroup(reactions[index].createdAt, reactions[index + 1].createdAt)) || index === reactions.length - 1} |             {#if (index != reactions.length - 1 && !shouldGroup(reactions[index].createdAt, reactions[index + 1].createdAt)) || index === reactions.length - 1} | ||||||
| @@ -240,27 +231,23 @@ | |||||||
|                   </a> |                   </a> | ||||||
|                 {/if} |                 {/if} | ||||||
|                 {#if reaction.user.id === user.id || albumOwnerId === user.id} |                 {#if reaction.user.id === user.id || albumOwnerId === user.id} | ||||||
|                   <div class="flex items-start w-fit"> |                   <div class="mr-4"> | ||||||
|                     <CircleIconButton |                     <ButtonContextMenu | ||||||
|                       icon={mdiDotsVertical} |                       icon={mdiDotsVertical} | ||||||
|                       title={$t('reaction_options')} |                       title={$t('reaction_options')} | ||||||
|  |                       align="top-right" | ||||||
|  |                       direction="left" | ||||||
|                       size="16" |                       size="16" | ||||||
|                       on:click={() => (showDeleteReaction[index] ? '' : showOptionsMenu(index))} |  | ||||||
|                     /> |  | ||||||
|                   </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 |                       <MenuOption | ||||||
|                     </button> |                         activeColor="bg-red-200" | ||||||
|                   {/if} |                         icon={mdiDeleteOutline} | ||||||
|  |                         text={$t('remove')} | ||||||
|  |                         onClick={() => handleDeleteReaction(reaction, index)} | ||||||
|  |                       /> | ||||||
|  |                     </ButtonContextMenu> | ||||||
|                   </div> |                   </div> | ||||||
|  |                 {/if} | ||||||
|               </div> |               </div> | ||||||
|               {#if (index != reactions.length - 1 && isTenMinutesApart(reactions[index].createdAt, reactions[index + 1].createdAt)) || index === reactions.length - 1} |               {#if (index != reactions.length - 1 && isTenMinutesApart(reactions[index].createdAt, reactions[index + 1].createdAt)) || index === reactions.length - 1} | ||||||
|                 <div |                 <div | ||||||
|   | |||||||
| @@ -177,65 +177,65 @@ | |||||||
|       /> |       /> | ||||||
|       <ButtonContextMenu direction="left" align="top-right" color="opaque" title={$t('more')} icon={mdiDotsVertical}> |       <ButtonContextMenu direction="left" align="top-right" color="opaque" title={$t('more')} icon={mdiDotsVertical}> | ||||||
|         {#if showSlideshow} |         {#if showSlideshow} | ||||||
|           <MenuOption icon={mdiPresentationPlay} on:click={() => onMenuClick('playSlideShow')} text={$t('slideshow')} /> |           <MenuOption icon={mdiPresentationPlay} onClick={() => onMenuClick('playSlideShow')} text={$t('slideshow')} /> | ||||||
|         {/if} |         {/if} | ||||||
|         {#if showDownloadButton} |         {#if showDownloadButton} | ||||||
|           <MenuOption icon={mdiFolderDownloadOutline} on:click={() => onMenuClick('download')} text={$t('download')} /> |           <MenuOption icon={mdiFolderDownloadOutline} onClick={() => onMenuClick('download')} text={$t('download')} /> | ||||||
|         {/if} |         {/if} | ||||||
|         {#if asset.isTrashed} |         {#if asset.isTrashed} | ||||||
|           <MenuOption icon={mdiHistory} on:click={() => onMenuClick('restoreAsset')} text={$t('restore')} /> |           <MenuOption icon={mdiHistory} onClick={() => onMenuClick('restoreAsset')} text={$t('restore')} /> | ||||||
|         {:else} |         {: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 |           <MenuOption | ||||||
|             icon={mdiShareVariantOutline} |             icon={mdiShareVariantOutline} | ||||||
|             on:click={() => onMenuClick('addToSharedAlbum')} |             onClick={() => onMenuClick('addToSharedAlbum')} | ||||||
|             text={$t('add_to_shared_album')} |             text={$t('add_to_shared_album')} | ||||||
|           /> |           /> | ||||||
|         {/if} |         {/if} | ||||||
|  |  | ||||||
|         {#if isOwner} |         {#if isOwner} | ||||||
|           {#if hasStackChildren} |           {#if hasStackChildren} | ||||||
|             <MenuOption icon={mdiImageMinusOutline} on:click={() => onMenuClick('unstack')} text={$t('unstack')} /> |             <MenuOption icon={mdiImageMinusOutline} onClick={() => onMenuClick('unstack')} text={$t('unstack')} /> | ||||||
|           {/if} |           {/if} | ||||||
|           {#if album} |           {#if album} | ||||||
|             <MenuOption |             <MenuOption | ||||||
|               text={$t('set_as_album_cover')} |               text={$t('set_as_album_cover')} | ||||||
|               icon={mdiImageOutline} |               icon={mdiImageOutline} | ||||||
|               on:click={() => onMenuClick('setAsAlbumCover')} |               onClick={() => onMenuClick('setAsAlbumCover')} | ||||||
|             /> |             /> | ||||||
|           {/if} |           {/if} | ||||||
|           {#if asset.type === AssetTypeEnum.Image} |           {#if asset.type === AssetTypeEnum.Image} | ||||||
|             <MenuOption |             <MenuOption | ||||||
|               icon={mdiAccountCircleOutline} |               icon={mdiAccountCircleOutline} | ||||||
|               on:click={() => onMenuClick('asProfileImage')} |               onClick={() => onMenuClick('asProfileImage')} | ||||||
|               text={$t('set_as_profile_picture')} |               text={$t('set_as_profile_picture')} | ||||||
|             /> |             /> | ||||||
|           {/if} |           {/if} | ||||||
|           <MenuOption |           <MenuOption | ||||||
|             on:click={() => onMenuClick('toggleArchive')} |             onClick={() => onMenuClick('toggleArchive')} | ||||||
|             icon={asset.isArchived ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline} |             icon={asset.isArchived ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline} | ||||||
|             text={asset.isArchived ? $t('unarchive') : $t('to_archive')} |             text={asset.isArchived ? $t('unarchive') : $t('to_archive')} | ||||||
|           /> |           /> | ||||||
|           <MenuOption |           <MenuOption | ||||||
|             icon={mdiUpload} |             icon={mdiUpload} | ||||||
|             on:click={() => openFileUploadDialog({ multiple: false, assetId: asset.id })} |             onClick={() => openFileUploadDialog({ multiple: false, assetId: asset.id })} | ||||||
|             text={$t('replace_with_upload')} |             text={$t('replace_with_upload')} | ||||||
|           /> |           /> | ||||||
|           <hr /> |           <hr /> | ||||||
|           <MenuOption |           <MenuOption | ||||||
|             icon={mdiDatabaseRefreshOutline} |             icon={mdiDatabaseRefreshOutline} | ||||||
|             on:click={() => onJobClick(AssetJobName.RefreshMetadata)} |             onClick={() => onJobClick(AssetJobName.RefreshMetadata)} | ||||||
|             text={getAssetJobName(AssetJobName.RefreshMetadata)} |             text={getAssetJobName(AssetJobName.RefreshMetadata)} | ||||||
|           /> |           /> | ||||||
|           <MenuOption |           <MenuOption | ||||||
|             icon={mdiImageRefreshOutline} |             icon={mdiImageRefreshOutline} | ||||||
|             on:click={() => onJobClick(AssetJobName.RegenerateThumbnail)} |             onClick={() => onJobClick(AssetJobName.RegenerateThumbnail)} | ||||||
|             text={getAssetJobName(AssetJobName.RegenerateThumbnail)} |             text={getAssetJobName(AssetJobName.RegenerateThumbnail)} | ||||||
|           /> |           /> | ||||||
|           {#if asset.type === AssetTypeEnum.Video} |           {#if asset.type === AssetTypeEnum.Video} | ||||||
|             <MenuOption |             <MenuOption | ||||||
|               icon={mdiCogRefreshOutline} |               icon={mdiCogRefreshOutline} | ||||||
|               on:click={() => onJobClick(AssetJobName.TranscodeVideo)} |               onClick={() => onJobClick(AssetJobName.TranscodeVideo)} | ||||||
|               text={getAssetJobName(AssetJobName.TranscodeVideo)} |               text={getAssetJobName(AssetJobName.TranscodeVideo)} | ||||||
|             /> |             /> | ||||||
|           {/if} |           {/if} | ||||||
|   | |||||||
| @@ -75,15 +75,15 @@ | |||||||
|       icon={mdiDotsVertical} |       icon={mdiDotsVertical} | ||||||
|       title={$t('show_person_options')} |       title={$t('show_person_options')} | ||||||
|     > |     > | ||||||
|       <MenuOption on:click={() => onMenuClick('hide-person')} icon={mdiEyeOffOutline} text={$t('hide_person')} /> |       <MenuOption onClick={() => onMenuClick('hide-person')} icon={mdiEyeOffOutline} text={$t('hide_person')} /> | ||||||
|       <MenuOption on:click={() => onMenuClick('change-name')} icon={mdiAccountEditOutline} text={$t('change_name')} /> |       <MenuOption onClick={() => onMenuClick('change-name')} icon={mdiAccountEditOutline} text={$t('change_name')} /> | ||||||
|       <MenuOption |       <MenuOption | ||||||
|         on:click={() => onMenuClick('set-birth-date')} |         onClick={() => onMenuClick('set-birth-date')} | ||||||
|         icon={mdiCalendarEditOutline} |         icon={mdiCalendarEditOutline} | ||||||
|         text={$t('set_date_of_birth')} |         text={$t('set_date_of_birth')} | ||||||
|       /> |       /> | ||||||
|       <MenuOption |       <MenuOption | ||||||
|         on:click={() => onMenuClick('merge-people')} |         onClick={() => onMenuClick('merge-people')} | ||||||
|         icon={mdiAccountMultipleCheckOutline} |         icon={mdiAccountMultipleCheckOutline} | ||||||
|         text={$t('merge_people')} |         text={$t('merge_people')} | ||||||
|       /> |       /> | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <MenuOption | <MenuOption | ||||||
|   on:click={() => (showAlbumPicker = true)} |   onClick={() => (showAlbumPicker = true)} | ||||||
|   text={shared ? $t('add_to_shared_album') : $t('add_to_album')} |   text={shared ? $t('add_to_shared_album') : $t('add_to_album')} | ||||||
|   icon={shared ? mdiShareVariantOutline : mdiImageAlbum} |   icon={shared ? mdiShareVariantOutline : mdiImageAlbum} | ||||||
| /> | /> | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| {#if menuItem} | {#if menuItem} | ||||||
|   <MenuOption {text} {icon} on:click={handleArchive} /> |   <MenuOption {text} {icon} onClick={handleArchive} /> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
| {#if !menuItem} | {#if !menuItem} | ||||||
|   | |||||||
| @@ -34,6 +34,6 @@ | |||||||
|  |  | ||||||
| {#each jobs as job} | {#each jobs as job} | ||||||
|   {#if isAllVideos || job !== AssetJobName.TranscodeVideo} |   {#if isAllVideos || job !== AssetJobName.TranscodeVideo} | ||||||
|     <MenuOption text={getAssetJobName(job)} icon={getAssetJobIcon(job)} on:click={() => handleRunJob(job)} /> |     <MenuOption text={getAssetJobName(job)} icon={getAssetJobIcon(job)} onClick={() => handleRunJob(job)} /> | ||||||
|   {/if} |   {/if} | ||||||
| {/each} | {/each} | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| {#if menuItem} | {#if menuItem} | ||||||
|   <MenuOption text={$t('change_date')} icon={mdiCalendarEditOutline} on:click={() => (isShowChangeDate = true)} /> |   <MenuOption text={$t('change_date')} icon={mdiCalendarEditOutline} onClick={() => (isShowChangeDate = true)} /> | ||||||
| {/if} | {/if} | ||||||
| {#if isShowChangeDate} | {#if isShowChangeDate} | ||||||
|   <ChangeDate |   <ChangeDate | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ | |||||||
|   <MenuOption |   <MenuOption | ||||||
|     text={$t('change_location')} |     text={$t('change_location')} | ||||||
|     icon={mdiMapMarkerMultipleOutline} |     icon={mdiMapMarkerMultipleOutline} | ||||||
|     on:click={() => (isShowChangeLocation = true)} |     onClick={() => (isShowChangeLocation = true)} | ||||||
|   /> |   /> | ||||||
| {/if} | {/if} | ||||||
| {#if isShowChangeLocation} | {#if isShowChangeLocation} | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| {#if menuItem} | {#if menuItem} | ||||||
|   <MenuOption text={label} icon={mdiDeleteOutline} on:click={handleTrash} /> |   <MenuOption text={label} icon={mdiDeleteOutline} onClick={handleTrash} /> | ||||||
| {:else if loading} | {:else if loading} | ||||||
|   <CircleIconButton title={$t('loading')} icon={mdiTimerSand} /> |   <CircleIconButton title={$t('loading')} icon={mdiTimerSand} /> | ||||||
| {:else} | {:else} | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| {#if menuItem} | {#if menuItem} | ||||||
|   <MenuOption text={$t('download')} icon={menuItemIcon} on:click={handleDownloadFiles} /> |   <MenuOption text={$t('download')} icon={menuItemIcon} onClick={handleDownloadFiles} /> | ||||||
| {:else} | {:else} | ||||||
|   <CircleIconButton title={$t('download')} icon={mdiCloudDownloadOutline} on:click={handleDownloadFiles} /> |   <CircleIconButton title={$t('download')} icon={mdiCloudDownloadOutline} on:click={handleDownloadFiles} /> | ||||||
| {/if} | {/if} | ||||||
|   | |||||||
| @@ -58,7 +58,7 @@ | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| {#if menuItem} | {#if menuItem} | ||||||
|   <MenuOption {text} {icon} on:click={handleFavorite} /> |   <MenuOption {text} {icon} onClick={handleFavorite} /> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
| {#if !menuItem} | {#if !menuItem} | ||||||
|   | |||||||
| @@ -57,7 +57,7 @@ | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| {#if menuItem} | {#if menuItem} | ||||||
|   <MenuOption text={$t('remove_from_album')} icon={mdiImageRemoveOutline} on:click={removeFromAlbum} /> |   <MenuOption text={$t('remove_from_album')} icon={mdiImageRemoveOutline} onClick={removeFromAlbum} /> | ||||||
| {:else} | {:else} | ||||||
|   <CircleIconButton title={$t('remove_from_album')} icon={mdiDeleteOutline} on:click={removeFromAlbum} /> |   <CircleIconButton title={$t('remove_from_album')} icon={mdiDeleteOutline} on:click={removeFromAlbum} /> | ||||||
| {/if} | {/if} | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ | |||||||
| </script> | </script> | ||||||
|  |  | ||||||
| {#if unstack} | {#if unstack} | ||||||
|   <MenuOption text={$t('unstack')} icon={mdiImageMinusOutline} on:click={handleUnstack} /> |   <MenuOption text={$t('unstack')} icon={mdiImageMinusOutline} onClick={handleUnstack} /> | ||||||
| {:else} | {:else} | ||||||
|   <MenuOption text={$t('stack')} icon={mdiImageMultipleOutline} on:click={handleStack} /> |   <MenuOption text={$t('stack')} icon={mdiImageMultipleOutline} onClick={handleStack} /> | ||||||
| {/if} | {/if} | ||||||
|   | |||||||
| @@ -1,24 +1,22 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
|   import Icon from '$lib/components/elements/icon.svelte'; |   import Icon from '$lib/components/elements/icon.svelte'; | ||||||
|   import { generateId } from '$lib/utils/generate-id'; |   import { generateId } from '$lib/utils/generate-id'; | ||||||
|   import { createEventDispatcher } from 'svelte'; |  | ||||||
|   import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store'; |   import { optionClickCallbackStore, selectedIdStore } from '$lib/stores/context-menu.store'; | ||||||
|  |  | ||||||
|   export let text = ''; |   export let text: string; | ||||||
|   export let subtitle = ''; |   export let subtitle = ''; | ||||||
|   export let icon = ''; |   export let icon = ''; | ||||||
|  |   export let activeColor = 'bg-slate-300'; | ||||||
|  |   export let textColor = 'text-immich-fg dark:text-immich-dark-bg'; | ||||||
|  |   export let onClick: () => void; | ||||||
|  |  | ||||||
|   let id: string = generateId(); |   let id: string = generateId(); | ||||||
|  |  | ||||||
|   $: isActive = $selectedIdStore === id; |   $: isActive = $selectedIdStore === id; | ||||||
|  |  | ||||||
|   const dispatch = createEventDispatcher<{ |  | ||||||
|     click: void; |  | ||||||
|   }>(); |  | ||||||
|  |  | ||||||
|   const handleClick = () => { |   const handleClick = () => { | ||||||
|     $optionClickCallbackStore?.(); |     $optionClickCallbackStore?.(); | ||||||
|     dispatch('click'); |     onClick(); | ||||||
|   }; |   }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| @@ -29,27 +27,20 @@ | |||||||
|   on:click={handleClick} |   on:click={handleClick} | ||||||
|   on:mouseover={() => ($selectedIdStore = id)} |   on:mouseover={() => ($selectedIdStore = id)} | ||||||
|   on:mouseleave={() => ($selectedIdStore = undefined)} |   on:mouseleave={() => ($selectedIdStore = undefined)} | ||||||
|   class="w-full p-4 text-left text-sm font-medium text-immich-fg focus:outline-none focus:ring-2 focus:ring-inset dark:text-immich-dark-bg cursor-pointer border-gray-200" |   class="w-full p-4 text-left text-sm font-medium {textColor} focus:outline-none focus:ring-2 focus:ring-inset cursor-pointer border-gray-200 flex gap-2 items-center {isActive | ||||||
|   class:bg-slate-300={isActive} |     ? activeColor | ||||||
|   class:bg-slate-100={!isActive} |     : 'bg-slate-100'}" | ||||||
|   role="menuitem" |   role="menuitem" | ||||||
| > | > | ||||||
|   {#if text} |  | ||||||
|   {#if icon} |   {#if icon} | ||||||
|       <p class="flex gap-2"> |  | ||||||
|     <Icon path={icon} ariaHidden={true} size="18" /> |     <Icon path={icon} ariaHidden={true} size="18" /> | ||||||
|         {text} |  | ||||||
|       </p> |  | ||||||
|     {:else} |  | ||||||
|       {text} |  | ||||||
|   {/if} |   {/if} | ||||||
|   {:else} |   <div> | ||||||
|     <slot /> |     {text} | ||||||
|   {/if} |     {#if subtitle} | ||||||
|  |  | ||||||
|   <slot name="subtitle"> |  | ||||||
|       <p class="text-xs text-gray-500"> |       <p class="text-xs text-gray-500"> | ||||||
|         {subtitle} |         {subtitle} | ||||||
|       </p> |       </p> | ||||||
|   </slot> |     {/if} | ||||||
|  |   </div> | ||||||
| </li> | </li> | ||||||
|   | |||||||
| @@ -426,7 +426,7 @@ | |||||||
|               <MenuOption |               <MenuOption | ||||||
|                 text={$t('set_as_album_cover')} |                 text={$t('set_as_album_cover')} | ||||||
|                 icon={mdiImageOutline} |                 icon={mdiImageOutline} | ||||||
|                 on:click={() => updateThumbnailUsingCurrentSelection()} |                 onClick={() => updateThumbnailUsingCurrentSelection()} | ||||||
|               /> |               /> | ||||||
|             {/if} |             {/if} | ||||||
|             <ArchiveAction menuItem unarchive={isAllArchived} onArchive={() => assetStore.triggerUpdate()} /> |             <ArchiveAction menuItem unarchive={isAllArchived} onArchive={() => assetStore.triggerUpdate()} /> | ||||||
| @@ -468,14 +468,10 @@ | |||||||
|                   <MenuOption |                   <MenuOption | ||||||
|                     icon={mdiImageOutline} |                     icon={mdiImageOutline} | ||||||
|                     text={$t('select_album_cover')} |                     text={$t('select_album_cover')} | ||||||
|                     on:click={() => (viewMode = ViewMode.SELECT_THUMBNAIL)} |                     onClick={() => (viewMode = ViewMode.SELECT_THUMBNAIL)} | ||||||
|                   /> |                   /> | ||||||
|                   <MenuOption |                   <MenuOption icon={mdiCogOutline} text={$t('options')} onClick={() => (viewMode = ViewMode.OPTIONS)} /> | ||||||
|                     icon={mdiCogOutline} |                   <MenuOption icon={mdiDeleteOutline} text={$t('delete_album')} onClick={() => handleRemoveAlbum()} /> | ||||||
|                     text={$t('options')} |  | ||||||
|                     on:click={() => (viewMode = ViewMode.OPTIONS)} |  | ||||||
|                   /> |  | ||||||
|                   <MenuOption icon={mdiDeleteOutline} text={$t('delete_album')} on:click={() => handleRemoveAlbum()} /> |  | ||||||
|                 </ButtonContextMenu> |                 </ButtonContextMenu> | ||||||
|               {/if} |               {/if} | ||||||
|             {/if} |             {/if} | ||||||
|   | |||||||
| @@ -390,7 +390,7 @@ | |||||||
|         <MenuOption |         <MenuOption | ||||||
|           icon={mdiAccountMultipleCheckOutline} |           icon={mdiAccountMultipleCheckOutline} | ||||||
|           text={$t('fix_incorrect_match')} |           text={$t('fix_incorrect_match')} | ||||||
|           on:click={handleReassignAssets} |           onClick={handleReassignAssets} | ||||||
|         /> |         /> | ||||||
|         <ChangeDate menuItem /> |         <ChangeDate menuItem /> | ||||||
|         <ChangeLocation menuItem /> |         <ChangeLocation menuItem /> | ||||||
| @@ -406,22 +406,22 @@ | |||||||
|             <MenuOption |             <MenuOption | ||||||
|               text={$t('select_featured_photo')} |               text={$t('select_featured_photo')} | ||||||
|               icon={mdiAccountBoxOutline} |               icon={mdiAccountBoxOutline} | ||||||
|               on:click={() => (viewMode = ViewMode.SELECT_PERSON)} |               onClick={() => (viewMode = ViewMode.SELECT_PERSON)} | ||||||
|             /> |             /> | ||||||
|             <MenuOption |             <MenuOption | ||||||
|               text={data.person.isHidden ? $t('unhide_person') : $t('hide_person')} |               text={data.person.isHidden ? $t('unhide_person') : $t('hide_person')} | ||||||
|               icon={data.person.isHidden ? mdiEyeOutline : mdiEyeOffOutline} |               icon={data.person.isHidden ? mdiEyeOutline : mdiEyeOffOutline} | ||||||
|               on:click={() => toggleHidePerson()} |               onClick={() => toggleHidePerson()} | ||||||
|             /> |             /> | ||||||
|             <MenuOption |             <MenuOption | ||||||
|               text={$t('set_date_of_birth')} |               text={$t('set_date_of_birth')} | ||||||
|               icon={mdiCalendarEditOutline} |               icon={mdiCalendarEditOutline} | ||||||
|               on:click={() => (viewMode = ViewMode.BIRTH_DATE)} |               onClick={() => (viewMode = ViewMode.BIRTH_DATE)} | ||||||
|             /> |             /> | ||||||
|             <MenuOption |             <MenuOption | ||||||
|               text={$t('merge_people')} |               text={$t('merge_people')} | ||||||
|               icon={mdiAccountMultipleCheckOutline} |               icon={mdiAccountMultipleCheckOutline} | ||||||
|               on:click={() => (viewMode = ViewMode.MERGE_PEOPLE)} |               onClick={() => (viewMode = ViewMode.MERGE_PEOPLE)} | ||||||
|             /> |             /> | ||||||
|           </ButtonContextMenu> |           </ButtonContextMenu> | ||||||
|         </svelte:fragment> |         </svelte:fragment> | ||||||
|   | |||||||
| @@ -380,29 +380,32 @@ | |||||||
|                     icon={mdiDotsVertical} |                     icon={mdiDotsVertical} | ||||||
|                     title={$t('library_options')} |                     title={$t('library_options')} | ||||||
|                   > |                   > | ||||||
|                     <MenuOption on:click={() => onRenameClicked(index)} text={$t('rename')} /> |                     <MenuOption onClick={() => onRenameClicked(index)} text={$t('rename')} /> | ||||||
|                     <MenuOption on:click={() => onEditImportPathClicked(index)} text={$t('edit_import_paths')} /> |                     <MenuOption onClick={() => onEditImportPathClicked(index)} text={$t('edit_import_paths')} /> | ||||||
|                     <MenuOption on:click={() => onScanSettingClicked(index)} text={$t('scan_settings')} /> |                     <MenuOption onClick={() => onScanSettingClicked(index)} text={$t('scan_settings')} /> | ||||||
|                     <hr /> |                     <hr /> | ||||||
|                     <MenuOption on:click={() => onScanNewLibraryClicked(library)} text={$t('scan_new_library_files')} /> |                     <MenuOption onClick={() => onScanNewLibraryClicked(library)} text={$t('scan_new_library_files')} /> | ||||||
|                     <MenuOption |                     <MenuOption | ||||||
|                       on:click={() => onScanAllLibraryFilesClicked(library)} |                       onClick={() => onScanAllLibraryFilesClicked(library)} | ||||||
|                       text={$t('scan_all_library_files')} |                       text={$t('scan_all_library_files')} | ||||||
|                       subtitle={$t('only_refreshes_modified_files')} |                       subtitle={$t('only_refreshes_modified_files')} | ||||||
|                     /> |                     /> | ||||||
|                     <MenuOption |                     <MenuOption | ||||||
|                       on:click={() => onForceScanAllLibraryFilesClicked(library)} |                       onClick={() => onForceScanAllLibraryFilesClicked(library)} | ||||||
|                       text={$t('force_re-scan_library_files')} |                       text={$t('force_re-scan_library_files')} | ||||||
|                       subtitle={$t('refreshes_every_file')} |                       subtitle={$t('refreshes_every_file')} | ||||||
|                     /> |                     /> | ||||||
|                     <hr /> |                     <hr /> | ||||||
|                     <MenuOption |                     <MenuOption | ||||||
|                       on:click={() => onRemoveOfflineFilesClicked(library)} |                       onClick={() => onRemoveOfflineFilesClicked(library)} | ||||||
|                       text={$t('remove_offline_files')} |                       text={$t('remove_offline_files')} | ||||||
|                     /> |                     /> | ||||||
|                     <MenuOption on:click={() => onDeleteLibraryClicked(library, index)}> |                     <MenuOption | ||||||
|                       <p class="text-red-600">{$t('delete_library')}</p> |                       text={$t('delete_library')} | ||||||
|                     </MenuOption> |                       activeColor="bg-red-200" | ||||||
|  |                       textColor="text-red-600" | ||||||
|  |                       onClick={() => onDeleteLibraryClicked(library, index)} | ||||||
|  |                     /> | ||||||
|                   </ButtonContextMenu> |                   </ButtonContextMenu> | ||||||
|                 </td> |                 </td> | ||||||
|               </tr> |               </tr> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user