diff --git a/web/src/lib/actions/context-menu-navigation.ts b/web/src/lib/actions/context-menu-navigation.ts new file mode 100644 index 0000000000000..3b45e7fe527f0 --- /dev/null +++ b/web/src/lib/actions/context-menu-navigation.ts @@ -0,0 +1,108 @@ +import { shortcuts } from '$lib/actions/shortcut'; +import { tick } from 'svelte'; +import type { Action } from 'svelte/action'; + +interface Options { + /** + * A function that is called when the dropdown should be closed. + */ + closeDropdown: () => void; + /** + * The container element that with direct children that should be navigated. + */ + container: HTMLElement; + /** + * Indicates if the dropdown is open. + */ + isOpen: boolean; + /** + * Override the default behavior for the escape key. + */ + onEscape?: (event: KeyboardEvent) => void; + /** + * A function that is called when the dropdown should be opened. + */ + openDropdown?: (event: KeyboardEvent) => void; + /** + * The id of the currently selected element. + */ + selectedId: string | undefined; + /** + * A function that is called when the selection changes, to notify consumers of the new selected id. + */ + selectionChanged: (id: string | undefined) => void; +} + +export const contextMenuNavigation: Action = (node, options: Options) => { + const getCurrentElement = () => { + const { container, selectedId: activeId } = options; + return container?.querySelector(`#${activeId}`) as HTMLElement | null; + }; + + const close = () => { + const { closeDropdown, selectionChanged } = options; + selectionChanged(undefined); + closeDropdown(); + }; + + const moveSelection = async (direction: 'up' | 'down', event: KeyboardEvent) => { + const { selectionChanged, container, openDropdown } = options; + if (openDropdown) { + openDropdown(event); + await tick(); + } + + const children = Array.from(container?.children).filter((child) => child.tagName !== 'HR') as HTMLElement[]; + if (children.length === 0) { + return; + } + + const currentEl = getCurrentElement(); + const currentIndex = currentEl ? children.indexOf(currentEl) : -1; + const directionFactor = (direction === 'up' ? -1 : 1) + (direction === 'up' && currentIndex === -1 ? 1 : 0); + const newIndex = (currentIndex + directionFactor + children.length) % children.length; + const selectedNode = children[newIndex]; + selectedNode?.scrollIntoView({ block: 'nearest' }); + + selectionChanged(selectedNode?.id); + }; + + const onEscape = (event: KeyboardEvent) => { + const { onEscape } = options; + if (onEscape) { + onEscape(event); + return; + } + event.stopPropagation(); + close(); + }; + + const handleClick = (event: KeyboardEvent) => { + const { selectedId, isOpen, closeDropdown } = options; + if (isOpen && !selectedId) { + closeDropdown(); + return; + } + if (!selectedId) { + void moveSelection('down', event); + return; + } + const currentEl = getCurrentElement(); + currentEl?.click(); + }; + + const { destroy } = shortcuts(node, [ + { shortcut: { key: 'ArrowUp' }, onShortcut: (event) => moveSelection('up', event) }, + { shortcut: { key: 'ArrowDown' }, onShortcut: (event) => moveSelection('down', event) }, + { shortcut: { key: 'Escape' }, onShortcut: (event) => onEscape(event) }, + { shortcut: { key: ' ' }, onShortcut: (event) => handleClick(event) }, + { shortcut: { key: 'Enter' }, onShortcut: (event) => handleClick(event) }, + ]); + + return { + update(newOptions) { + options = newOptions; + }, + destroy, + }; +}; diff --git a/web/src/lib/actions/focus-outside.ts b/web/src/lib/actions/focus-outside.ts index c8a4d574cbb9a..07a85b021eb4f 100644 --- a/web/src/lib/actions/focus-outside.ts +++ b/web/src/lib/actions/focus-outside.ts @@ -1,5 +1,5 @@ interface Options { - onFocusOut?: () => void; + onFocusOut?: (event: FocusEvent) => void; } export function focusOutside(node: HTMLElement, options: Options = {}) { @@ -7,7 +7,7 @@ export function focusOutside(node: HTMLElement, options: Options = {}) { const handleFocusOut = (event: FocusEvent) => { if (onFocusOut && event.relatedTarget instanceof Node && !node.contains(event.relatedTarget as Node)) { - onFocusOut(); + onFocusOut(event); } }; diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte index 6ff723d3bb50e..b5369337387c3 100644 --- a/web/src/lib/components/album-page/album-card.svelte +++ b/web/src/lib/components/album-page/album-card.svelte @@ -3,7 +3,7 @@ import { user } from '$lib/stores/user.store'; import type { AlbumResponseDto } from '@immich/sdk'; import { mdiDotsVertical } from '@mdi/js'; - import { getContextMenuPosition, type ContextMenuPosition } from '$lib/utils/context-menu'; + import { getContextMenuPositionFromEvent, type ContextMenuPosition } from '$lib/utils/context-menu'; import { getShortDateRange } from '$lib/utils/date-time'; import AlbumCover from '$lib/components/album-page/album-cover.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; @@ -20,7 +20,7 @@ const showAlbumContextMenu = (e: MouseEvent) => { e.stopPropagation(); e.preventDefault(); - onShowContextMenu?.(getContextMenuPosition(e)); + onShowContextMenu?.(getContextMenuPositionFromEvent(e)); }; diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte index 9c155b68e4b16..a5802bf13b5d0 100644 --- a/web/src/lib/components/album-page/albums-list.svelte +++ b/web/src/lib/components/album-page/albums-list.svelte @@ -3,7 +3,6 @@ import { groupBy, orderBy } from 'lodash-es'; import { addUsersToAlbum, deleteAlbum, type AlbumUserAddDto, type AlbumResponseDto, isHttpError } from '@immich/sdk'; import { mdiDeleteOutline, mdiShareVariantOutline, mdiFolderDownloadOutline, mdiRenameOutline } from '@mdi/js'; - import Icon from '$lib/components/elements/icon.svelte'; import EditAlbumForm from '$lib/components/forms/edit-album-form.svelte'; import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte'; import { @@ -167,6 +166,7 @@ let contextMenuPosition: ContextMenuPosition = { x: 0, y: 0 }; let contextMenuTargetAlbum: AlbumResponseDto | null = null; + let isOpen = false; // Step 1: Filter between Owned and Shared albums, or both. $: { @@ -224,7 +224,6 @@ albumGroupIds = groupedAlbums.map(({ id }) => id); } - $: showContextMenu = !!contextMenuTargetAlbum; $: showFullContextMenu = allowEdit && contextMenuTargetAlbum && contextMenuTargetAlbum.ownerId === $user.id; onMount(async () => { @@ -253,10 +252,11 @@ x: contextMenuDetail.x, y: contextMenuDetail.y, }; + isOpen = true; }; const closeAlbumContextMenu = () => { - contextMenuTargetAlbum = null; + isOpen = false; }; const handleDownloadAlbum = async () => { @@ -419,34 +419,18 @@ {/if} - + {#if showFullContextMenu} - contextMenuTargetAlbum && handleEdit(contextMenuTargetAlbum)}> -

- - Edit -

-
- openShareModal()}> -

- - Share -

-
+ contextMenuTargetAlbum && handleEdit(contextMenuTargetAlbum)} + /> + openShareModal()} /> {/if} - handleDownloadAlbum()}> -

- - Download -

-
+ handleDownloadAlbum()} /> {#if showFullContextMenu} - setAlbumToDelete()}> -

- - Delete -

-
+ setAlbumToDelete()} /> {/if}
diff --git a/web/src/lib/components/album-page/share-info-modal.svelte b/web/src/lib/components/album-page/share-info-modal.svelte index c395053c70fc0..82403efcbe21b 100644 --- a/web/src/lib/components/album-page/share-info-modal.svelte +++ b/web/src/lib/components/album-page/share-info-modal.svelte @@ -9,16 +9,14 @@ } from '@immich/sdk'; import { mdiDotsVertical } from '@mdi/js'; import { createEventDispatcher, onMount } from 'svelte'; - import { getContextMenuPosition } from '../../utils/context-menu'; import { handleError } from '../../utils/handle-error'; - import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte'; - import ContextMenu from '../shared-components/context-menu/context-menu.svelte'; import MenuOption from '../shared-components/context-menu/menu-option.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; import UserAvatar from '../shared-components/user-avatar.svelte'; import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import { t } from 'svelte-i18n'; + import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; export let album: AlbumResponseDto; export let onClose: () => void; @@ -29,8 +27,6 @@ }>(); let currentUser: UserResponseDto; - let position = { x: 0, y: 0 }; - let selectedMenuUser: UserResponseDto | null = null; let selectedRemoveUser: UserResponseDto | null = null; $: isOwned = currentUser?.id == album.ownerId; @@ -43,15 +39,8 @@ } }); - const showContextMenu = (event: MouseEvent, user: UserResponseDto) => { - position = getContextMenuPosition(event); - selectedMenuUser = user; - selectedRemoveUser = null; - }; - - const handleMenuRemove = () => { - selectedRemoveUser = selectedMenuUser; - selectedMenuUser = null; + const handleMenuRemove = (user: UserResponseDto) => { + selectedRemoveUser = user; }; const handleRemoveUser = async () => { @@ -118,31 +107,17 @@ {/if} {#if isOwned} -
- showContextMenu(event, user)} - icon={mdiDotsVertical} - size="20" - /> - - {#if selectedMenuUser === user} - (selectedMenuUser = null)}> - {#if role === AlbumUserRole.Viewer} - handleSetReadonly(user, AlbumUserRole.Editor)} - text={$t('allow_edits')} - /> - {:else} - handleSetReadonly(user, AlbumUserRole.Viewer)} - text={$t('disallow_edits')} - /> - {/if} - - + + {#if role === AlbumUserRole.Viewer} + handleSetReadonly(user, AlbumUserRole.Editor)} text={$t('allow_edits')} /> + {:else} + handleSetReadonly(user, AlbumUserRole.Viewer)} + text={$t('disallow_edits')} + /> {/if} -
+ handleMenuRemove(user)} text={$t('remove')} /> + {:else if user.id == currentUser?.id} + diff --git a/web/src/lib/components/shared-components/context-menu/menu.ts b/web/src/lib/components/shared-components/context-menu/menu.ts deleted file mode 100644 index 108b2fd1aadd6..0000000000000 --- a/web/src/lib/components/shared-components/context-menu/menu.ts +++ /dev/null @@ -1,3 +0,0 @@ -const key = {}; - -export { key }; diff --git a/web/src/lib/components/shared-components/context-menu/right-click-context-menu.svelte b/web/src/lib/components/shared-components/context-menu/right-click-context-menu.svelte index 4d7f38b5b26e3..f0b0408ff9950 100644 --- a/web/src/lib/components/shared-components/context-menu/right-click-context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/right-click-context-menu.svelte @@ -1,7 +1,12 @@ {#key uniqueKey} {#if isOpen} - + + {/if} {/key} diff --git a/web/src/lib/stores/context-menu.store.ts b/web/src/lib/stores/context-menu.store.ts new file mode 100644 index 0000000000000..534c15bd47f67 --- /dev/null +++ b/web/src/lib/stores/context-menu.store.ts @@ -0,0 +1,6 @@ +import { writable } from 'svelte/store'; + +const selectedIdStore = writable(undefined); +const optionClickCallbackStore = writable<(() => void) | undefined>(undefined); + +export { optionClickCallbackStore, selectedIdStore }; diff --git a/web/src/lib/utils/context-menu.ts b/web/src/lib/utils/context-menu.ts index 54ae1a3374ea2..aca1033c7a433 100644 --- a/web/src/lib/utils/context-menu.ts +++ b/web/src/lib/utils/context-menu.ts @@ -2,22 +2,31 @@ export type Align = 'middle' | 'top-left' | 'top-right'; export type ContextMenuPosition = { x: number; y: number }; -export const getContextMenuPosition = (event: MouseEvent, align: Align = 'middle'): ContextMenuPosition => { - const { x, y, currentTarget, target } = event; +export const getContextMenuPositionFromEvent = ( + event: MouseEvent | KeyboardEvent, + align: Align = 'middle', +): ContextMenuPosition => { + const { currentTarget, target } = event; + const x = 'x' in event ? event.x : 0; + const y = 'y' in event ? event.y : 0; const box = ((currentTarget || target) as HTMLElement)?.getBoundingClientRect(); if (box) { - switch (align) { - case 'middle': { - return { x: box.x + box.width / 2, y: box.y + box.height / 2 }; - } - case 'top-left': { - return { x: box.x, y: box.y }; - } - case 'top-right': { - return { x: box.x + box.width, y: box.y }; - } - } + return getContextMenuPositionFromBoundingRect(box, align); } return { x, y }; }; + +export const getContextMenuPositionFromBoundingRect = (rect: DOMRect, align: Align = 'middle'): ContextMenuPosition => { + switch (align) { + case 'middle': { + return { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }; + } + case 'top-left': { + return { x: rect.x, y: rect.y }; + } + case 'top-right': { + return { x: rect.x + rect.width, y: rect.y }; + } + } +}; diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index ef6c48814bcc8..7d0dbf0750c1f 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -22,9 +22,8 @@ import RemoveFromAlbum from '$lib/components/photos-page/actions/remove-from-album.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; - import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; + import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; - import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte'; @@ -43,8 +42,6 @@ import { user } from '$lib/stores/user.store'; import { handlePromiseError, s } from '$lib/utils'; import { downloadAlbum } from '$lib/utils/asset-utils'; - import { clickOutside } from '$lib/actions/click-outside'; - import { getContextMenuPosition } from '$lib/utils/context-menu'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { handleError } from '$lib/utils/handle-error'; import { isAlbumsRoute, isPeopleRoute, isSearchRoute } from '$lib/utils/navigation'; @@ -103,7 +100,6 @@ SELECT_USERS = 'select-users', SELECT_THUMBNAIL = 'select-thumbnail', SELECT_ASSETS = 'select-assets', - ALBUM_OPTIONS = 'album-options', VIEW_USERS = 'view-users', VIEW = 'view', OPTIONS = 'options', @@ -112,7 +108,6 @@ let backUrl: string = AppRoute.ALBUMS; let viewMode = ViewMode.VIEW; let isCreatingSharedAlbum = false; - let contextMenuPosition: { x: number; y: number } = { x: 0, y: 0 }; let isShowActivity = false; let isLiked: ActivityResponseDto | null = null; let reactions: ActivityResponseDto[] = []; @@ -305,11 +300,6 @@ timelineInteractionStore.clearMultiselect(); }; - const handleOpenAlbumOptions = (event: MouseEvent) => { - contextMenuPosition = getContextMenuPosition(event, 'top-left'); - viewMode = viewMode === ViewMode.VIEW ? ViewMode.ALBUM_OPTIONS : ViewMode.VIEW; - }; - const handleSelectFromComputer = async () => { await openFileUploadDialog({ albumId: album.id }); timelineInteractionStore.clearMultiselect(); @@ -420,14 +410,14 @@ assetInteractionStore.clearMultiselect()}> - + - + {#if isAllUserOwned} assetStore.triggerUpdate()} /> {/if} - + {#if isAllUserOwned} @@ -447,10 +437,10 @@ {#if isAllUserOwned} {/if} - + {:else} - {#if viewMode === ViewMode.VIEW || viewMode === ViewMode.ALBUM_OPTIONS} + {#if viewMode === ViewMode.VIEW} goto(backUrl)}> {#if isEditor} @@ -474,32 +464,19 @@ {#if isOwned} -
(viewMode = ViewMode.VIEW) }}> - + (viewMode = ViewMode.SELECT_THUMBNAIL)} /> - {#if viewMode === ViewMode.ALBUM_OPTIONS} - - (viewMode = ViewMode.SELECT_THUMBNAIL)} - /> - (viewMode = ViewMode.OPTIONS)} - /> - handleRemoveAlbum()} - /> - - {/if} -
+ (viewMode = ViewMode.OPTIONS)} + /> + handleRemoveAlbum()} /> + {/if} {/if} diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte index 26c0c6e65cbe6..6e3fb4cb28018 100644 --- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -8,7 +8,7 @@ import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; - import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; + import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { AssetAction } from '$lib/constants'; @@ -32,15 +32,15 @@ assetStore.removeAssets(assetIds)} /> - + - + assetStore.triggerUpdate()} /> - + assetStore.removeAssets(assetIds)} /> - + {/if} diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index 95ed078c72998..49af165ac99c7 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -10,7 +10,7 @@ import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; - import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; + import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { AssetAction } from '$lib/constants'; @@ -35,17 +35,17 @@ assetStore.removeAssets(assetIds)} /> - + - - + + assetStore.removeAssets(assetIds)} /> assetStore.removeAssets(assetIds)} /> - + {/if} diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 0f8fc244d299c..83e2ba3c1f24a 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -4,7 +4,7 @@ import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; - import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; + import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import { AppRoute } from '$lib/constants'; @@ -30,10 +30,10 @@ {#if $isMultiSelectState} - + - + {:else} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 7de385f47de62..3238d2ddc7eed 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -17,7 +17,6 @@ import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; - import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; @@ -57,6 +56,7 @@ import type { PageData } from './$types'; import { listNavigation } from '$lib/actions/list-navigation'; import { t } from 'svelte-i18n'; + import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; export let data: PageData; @@ -380,12 +380,12 @@ assetInteractionStore.clearMultiselect()}> - + - + assetStore.triggerUpdate()} /> - + $assetStore.removeAssets(assetIds)} /> $assetStore.removeAssets(assetIds)} /> - + {:else} {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE} goto(previousRoute)}> - + (viewMode = ViewMode.MERGE_PEOPLE)} /> - + {/if} diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index 3fed3ba1ca681..85a497fa99a71 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -12,7 +12,7 @@ import StackAction from '$lib/components/photos-page/actions/stack-action.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; - import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; + import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import MemoryLane from '$lib/components/photos-page/memory-lane.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; @@ -58,12 +58,12 @@ > - + - + assetStore.triggerUpdate()} /> - + {#if $selectedAssets.size > 1 || isAssetStackSelected} assetStore.removeAssets(assetIds)} />
-
+ {/if} diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index b947720972bc6..7723a86b41102 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -11,7 +11,7 @@ import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; - import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; + import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; @@ -211,19 +211,19 @@ (selectedAssets = new Set())}> - + - + - + - + {:else} diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index dbf2092edc42d..adb77a1e88c3a 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -6,16 +6,13 @@ import LibraryScanSettingsForm from '$lib/components/forms/library-scan-settings-form.svelte'; import LibraryUserPickerForm from '$lib/components/forms/library-user-picker-form.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; - import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import { notificationController, NotificationType, } from '$lib/components/shared-components/notification/notification'; - import Portal from '$lib/components/shared-components/portal/portal.svelte'; import { ByteUnit, getBytesWithUnit } from '$lib/utils/byte-units'; - import { getContextMenuPosition } from '$lib/utils/context-menu'; import { handleError } from '$lib/utils/handle-error'; import { createLibrary, @@ -35,9 +32,9 @@ import { fade, slide } from 'svelte/transition'; import LinkButton from '../../../lib/components/elements/buttons/link-button.svelte'; import type { PageData } from './$types'; - import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { t } from 'svelte-i18n'; + import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; export let data: PageData; @@ -63,10 +60,6 @@ let deleteAssetCount = 0; let dropdownOpen: boolean[] = []; - let showContextMenu = false; - let contextMenuPosition = { x: 0, y: 0 }; - let selectedLibraryIndex = 0; - let selectedLibrary: LibraryResponseDto | null = null; let toCreateLibrary = false; @@ -79,25 +72,12 @@ editScanSettings = null; renameLibrary = null; updateLibraryIndex = null; - showContextMenu = false; for (let index = 0; index < dropdownOpen.length; index++) { dropdownOpen[index] = false; } }; - const showMenu = (event: MouseEvent, library: LibraryResponseDto, index: number) => { - contextMenuPosition = getContextMenuPosition(event); - showContextMenu = !showContextMenu; - - selectedLibraryIndex = index; - selectedLibrary = library; - }; - - const onMenuExit = () => { - showContextMenu = false; - }; - const refreshStats = async (listIndex: number) => { stats[listIndex] = await getLibraryStatistics({ id: libraries[listIndex].id }); owner[listIndex] = await getUserAdmin({ id: libraries[listIndex].ownerId }); @@ -233,72 +213,72 @@ } }; - const onRenameClicked = () => { + const onRenameClicked = (index: number) => { closeAll(); - renameLibrary = selectedLibraryIndex; - updateLibraryIndex = selectedLibraryIndex; + renameLibrary = index; + updateLibraryIndex = index; }; - const onEditImportPathClicked = () => { + const onEditImportPathClicked = (index: number) => { closeAll(); - editImportPaths = selectedLibraryIndex; - updateLibraryIndex = selectedLibraryIndex; + editImportPaths = index; + updateLibraryIndex = index; }; - const onScanNewLibraryClicked = async () => { + const onScanNewLibraryClicked = async (library: LibraryResponseDto) => { closeAll(); - if (selectedLibrary) { - await handleScan(selectedLibrary.id); + if (library) { + await handleScan(library.id); } }; - const onScanSettingClicked = () => { + const onScanSettingClicked = (index: number) => { closeAll(); - editScanSettings = selectedLibraryIndex; - updateLibraryIndex = selectedLibraryIndex; + editScanSettings = index; + updateLibraryIndex = index; }; - const onScanAllLibraryFilesClicked = async () => { + const onScanAllLibraryFilesClicked = async (library: LibraryResponseDto) => { closeAll(); - if (selectedLibrary) { - await handleScanChanges(selectedLibrary.id); + if (library) { + await handleScanChanges(library.id); } }; - const onForceScanAllLibraryFilesClicked = async () => { + const onForceScanAllLibraryFilesClicked = async (library: LibraryResponseDto) => { closeAll(); - if (selectedLibrary) { - await handleForceScan(selectedLibrary.id); + if (library) { + await handleForceScan(library.id); } }; - const onRemoveOfflineFilesClicked = async () => { + const onRemoveOfflineFilesClicked = async (library: LibraryResponseDto) => { closeAll(); - if (selectedLibrary) { - await handleRemoveOffline(selectedLibrary.id); + if (library) { + await handleRemoveOffline(library.id); } }; - const onDeleteLibraryClicked = async () => { + const onDeleteLibraryClicked = async (library: LibraryResponseDto, index: number) => { closeAll(); - if (!selectedLibrary) { + if (!library) { return; } const isConfirmedLibrary = await dialogController.show({ id: 'delete-library', - prompt: $t('admin.confirm_delete_library', { values: { library: selectedLibrary.name } }), + prompt: $t('admin.confirm_delete_library', { values: { library: library.name } }), }); if (!isConfirmedLibrary) { return; } - await refreshStats(selectedLibraryIndex); - if (totalCount[selectedLibraryIndex] > 0) { - deleteAssetCount = totalCount[selectedLibraryIndex]; + await refreshStats(index); + if (totalCount[index] > 0) { + deleteAssetCount = totalCount[index]; const isConfirmedLibraryAssetCount = await dialogController.show({ id: 'delete-library-assets', @@ -310,7 +290,7 @@ } await handleDelete(); } else { - deletedLibrary = selectedLibrary; + deletedLibrary = library; await handleDelete(); } }; @@ -392,46 +372,38 @@ {/if} - showMenu(e, library, index)} - /> - - {#if showContextMenu} - - onMenuExit()}> - onRenameClicked()} text={$t('rename')} /> - - {#if selectedLibrary} - onEditImportPathClicked()} text={$t('edit_import_paths')} /> - onScanSettingClicked()} text={$t('scan_settings')} /> -
- onScanNewLibraryClicked()} text={$t('scan_new_library_files')} /> - onScanAllLibraryFilesClicked()} - text={$t('scan_all_library_files')} - subtitle={$t('only_refreshes_modified_files')} - /> - onForceScanAllLibraryFilesClicked()} - text={$t('force_re-scan_library_files')} - subtitle={$t('refreshes_every_file')} - /> -
- onRemoveOfflineFilesClicked()} - text={$t('remove_offline_files')} - /> - onDeleteLibraryClicked()}> -

{$t('delete_library')}

-
- {/if} -
-
- {/if} + > + onRenameClicked(index)} text={$t('rename')} /> + onEditImportPathClicked(index)} text={$t('edit_import_paths')} /> + onScanSettingClicked(index)} text={$t('scan_settings')} /> +
+ onScanNewLibraryClicked(library)} text={$t('scan_new_library_files')} /> + onScanAllLibraryFilesClicked(library)} + text={$t('scan_all_library_files')} + subtitle={$t('only_refreshes_modified_files')} + /> + onForceScanAllLibraryFilesClicked(library)} + text={$t('force_re-scan_library_files')} + subtitle={$t('refreshes_every_file')} + /> +
+ onRemoveOfflineFilesClicked(library)} + text={$t('remove_offline_files')} + /> + onDeleteLibraryClicked(library, index)}> +

{$t('delete_library')}

+
+ {#if renameLibrary === index}