From 683f503647c5ccda445403e156dfe910f72b4a82 Mon Sep 17 00:00:00 2001 From: martabal <74269598+martabal@users.noreply.github.com> Date: Sat, 25 May 2024 12:58:49 +0200 Subject: [PATCH] feat: use shortcut to favorite and archive on asset-grid --- web/src/lib/actions/focus.ts | 4 +- .../photos-page/actions/archive-action.svelte | 43 ++++--------------- .../actions/favorite-action.svelte | 41 +++--------------- .../components/photos-page/asset-grid.svelte | 42 ++++++++++++++---- web/src/lib/utils/actions.ts | 35 ++++++++++++++- web/src/lib/utils/asset-utils.ts | 7 +++ .../[[assetId=id]]/+page.svelte | 8 +++- .../[[assetId=id]]/+page.svelte | 8 +++- .../[[assetId=id]]/+page.svelte | 6 ++- web/src/routes/(user)/people/+page.svelte | 6 +-- .../[[assetId=id]]/+page.svelte | 7 ++- .../(user)/photos/[[assetId=id]]/+page.svelte | 7 ++- 12 files changed, 121 insertions(+), 93 deletions(-) diff --git a/web/src/lib/actions/focus.ts b/web/src/lib/actions/focus.ts index 81185625f7..78413d8187 100644 --- a/web/src/lib/actions/focus.ts +++ b/web/src/lib/actions/focus.ts @@ -1,3 +1,3 @@ -export const initInput = (element: HTMLInputElement) => { - element.focus(); +export const initInput = (element: HTMLInputElement, timeOut: number = 0) => { + setTimeout(() => element.focus(), timeOut); }; diff --git a/web/src/lib/components/photos-page/actions/archive-action.svelte b/web/src/lib/components/photos-page/actions/archive-action.svelte index f111db8036..7a52076ecd 100644 --- a/web/src/lib/components/photos-page/actions/archive-action.svelte +++ b/web/src/lib/components/photos-page/actions/archive-action.svelte @@ -1,15 +1,9 @@ diff --git a/web/src/lib/components/photos-page/actions/favorite-action.svelte b/web/src/lib/components/photos-page/actions/favorite-action.svelte index 520dd46564..8b5ab6a4ee 100644 --- a/web/src/lib/components/photos-page/actions/favorite-action.svelte +++ b/web/src/lib/components/photos-page/actions/favorite-action.svelte @@ -1,15 +1,9 @@ diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index b2ead89028..1545f19551 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -7,22 +7,23 @@ import { locale, showDeleteModal } from '$lib/stores/preferences.store'; import { isSearchEnabled } from '$lib/stores/search.store'; import { featureFlags } from '$lib/stores/server-config.store'; - import { deleteAssets } from '$lib/utils/actions'; + import { archiveAssets, deleteAssets, favoriteAssets } from '$lib/utils/actions'; import { type ShortcutOptions, shortcuts } from '$lib/actions/shortcut'; import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util'; import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; import { DateTime } from 'luxon'; import { createEventDispatcher, onDestroy, onMount } from 'svelte'; - import IntersectionObserver from '../asset-viewer/intersection-observer.svelte'; - import Portal from '../shared-components/portal/portal.svelte'; - import Scrollbar from '../shared-components/scrollbar/scrollbar.svelte'; - import ShowShortcuts from '../shared-components/show-shortcuts.svelte'; - import AssetDateGroup from './asset-date-group.svelte'; - import { stackAssets } from '$lib/utils/asset-utils'; - import DeleteAssetDialog from './delete-asset-dialog.svelte'; + import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte'; + import Portal from '$lib/components/shared-components/portal/portal.svelte'; + import Scrollbar from '$lib/components/shared-components/scrollbar/scrollbar.svelte'; + import ShowShortcuts from '$lib/components/shared-components/show-shortcuts.svelte'; + import AssetDateGroup from '$lib/components/photos-page/asset-date-group.svelte'; + import { handleFavoriteAssetGrid, stackAssets } from '$lib/utils/asset-utils'; + import DeleteAssetDialog from '$lib/components/photos-page/delete-asset-dialog.svelte'; import { handlePromiseError } from '$lib/utils'; import { selectAllAssets } from '$lib/utils/asset-utils'; import { navigate } from '$lib/utils/navigation'; + import { page } from '$app/stores'; export let isSelectionMode = false; export let singleSelect = false; @@ -34,6 +35,7 @@ export let isShared = false; export let album: AlbumResponseDto | null = null; export let isShowDeleteConfirmation = false; + export let isAllFavorite: boolean | null = null; $: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash; @@ -93,6 +95,22 @@ handlePromiseError(trashOrDelete(true)); }; + const onArchive = async () => { + const assets = Array.from($selectedAssets); + const ids = assets.map((asset) => asset.id); + await archiveAssets($page.url.pathname !== AppRoute.ARCHIVE, (assetIds) => assetStore.removeAssets(assetIds), ids); + assetInteractionStore.clearMultiselect(); + }; + + const onFavorite = async () => { + await favoriteAssets( + !isAllFavorite, + (assets, isFavorite) => handleFavoriteAssetGrid(assets, isFavorite, assetStore), + Array.from($selectedAssets), + ); + assetInteractionStore.clearMultiselect(); + }; + const onStackAssets = async () => { const ids = await stackAssets(Array.from($selectedAssets)); if (ids) { @@ -128,6 +146,14 @@ { shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() }, { shortcut: { key: 's' }, onShortcut: () => onStackAssets() }, ); + // you're not supposed to manage your assets on the trash page: there's no button to manage it, then there's no reason to have shortcuts to do it. + if ($page.url.pathname !== AppRoute.TRASH) { + shortcuts.push({ shortcut: { key: 'a', shift: true }, onShortcut: onArchive }); + // on some page, you can't favorite assets (like on `/parthner`) + if (isAllFavorite !== null) { + shortcuts.push({ shortcut: { key: 'f' }, onShortcut: onFavorite }); + } + } } return shortcuts; diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts index ecfd29a8fc..4c55487999 100644 --- a/web/src/lib/utils/actions.ts +++ b/web/src/lib/utils/actions.ts @@ -1,11 +1,11 @@ import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification'; -import { deleteAssets as deleteBulk, type AssetResponseDto } from '@immich/sdk'; +import { deleteAssets as deleteBulk, updateAssets, type AssetResponseDto } from '@immich/sdk'; import { handleError } from './handle-error'; export type OnDelete = (assetIds: string[]) => void; export type OnRestore = (ids: string[]) => void; export type OnArchive = (ids: string[], isArchived: boolean) => void; -export type OnFavorite = (ids: string[], favorite: boolean) => void; +export type OnFavorite = (assets: AssetResponseDto[], isFavorite: boolean) => void; export type OnStack = (ids: string[]) => void; export type OnUnstack = (assets: AssetResponseDto[]) => void; @@ -22,3 +22,34 @@ export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: handleError(error, 'Error deleting assets'); } }; + +export const favoriteAssets = async (isFavorite: boolean, onAssetFavorite: OnFavorite, assets: AssetResponseDto[]) => { + try { + const ids = assets.map((asset) => asset.id); + await updateAssets({ assetBulkUpdateDto: { ids, isFavorite } }); + + onAssetFavorite(assets, isFavorite); + + notificationController.show({ + message: isFavorite ? `Added ${ids.length} to favorites` : `Removed ${ids.length} from favorites`, + type: NotificationType.Info, + }); + } catch (error) { + handleError(error, `Unable to ${isFavorite ? 'add to' : 'remove from'} favorites`); + } +}; + +export const archiveAssets = async (isArchived: boolean, onAssetArchive: OnArchive, ids: string[]) => { + try { + await updateAssets({ assetBulkUpdateDto: { ids, isArchived } }); + + onAssetArchive(ids, isArchived); + + notificationController.show({ + message: `${isArchived ? 'Archived' : 'Unarchived'} ${ids.length}`, + type: NotificationType.Info, + }); + } catch (error) { + handleError(error, `Unable to ${isArchived ? 'archive' : 'unarchive'}`); + } +}; diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index f94c8b4375..ae2d9028c9 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -397,3 +397,10 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt export const delay = async (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; + +export const handleFavoriteAssetGrid = (assets: AssetResponseDto[], isFavorite: boolean, assetStore: AssetStore) => { + for (const asset of assets) { + asset.isFavorite = isFavorite; + } + assetStore.triggerUpdate(); +}; 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 32fb7c01ee..69513ceb06 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 @@ -43,7 +43,7 @@ import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { user } from '$lib/stores/user.store'; import { handlePromiseError, s } from '$lib/utils'; - import { downloadAlbum } from '$lib/utils/asset-utils'; + import { downloadAlbum, handleFavoriteAssetGrid } 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'; @@ -412,7 +412,10 @@ {#if isAllUserOwned} - assetStore.triggerUpdate()} /> + handleFavoriteAssetGrid(assets, isFavorite, assetStore)} + /> {/if} @@ -546,6 +549,7 @@ {album} {assetStore} {assetInteractionStore} + {isAllFavorite} isShared={album.albumUsers.length > 0} isSelectionMode={viewMode === ViewMode.SELECT_THUMBNAIL} singleSelect={viewMode === ViewMode.SELECT_THUMBNAIL} 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 2f943a4ffd..dff88e9d4f 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 @@ -14,6 +14,7 @@ import { AssetAction } from '$lib/constants'; import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { AssetStore } from '$lib/stores/assets.store'; + import { handleFavoriteAssetGrid } from '$lib/utils/asset-utils'; import type { PageData } from './$types'; import { mdiPlus, mdiDotsVertical } from '@mdi/js'; @@ -35,7 +36,10 @@ - assetStore.triggerUpdate()} /> + handleFavoriteAssetGrid(assets, isFavorite, assetStore)} + /> assetStore.removeAssets(assetIds)} /> @@ -44,7 +48,7 @@ {/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 1d080d1003..92730326ef 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 @@ -16,6 +16,7 @@ import { AssetAction } from '$lib/constants'; import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { AssetStore } from '$lib/stores/assets.store'; + import { handleFavoriteAssetGrid } from '$lib/utils/asset-utils'; import type { PageData } from './$types'; import { mdiDotsVertical, mdiPlus } from '@mdi/js'; @@ -31,7 +32,10 @@ {#if $isMultiSelectState} assetInteractionStore.clearMultiselect()}> - assetStore.removeAssets(assetIds)} /> + handleFavoriteAssetGrid(assets, isFavorite, assetStore)} + /> diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index e1fdb9cacc..f4aad4c79d 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -34,6 +34,7 @@ import { clearQueryParam } from '$lib/utils/navigation'; import SearchPeople from '$lib/components/faces-page/people-search.svelte'; import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; + import { initInput } from '$lib/actions/focus'; export let data: PageData; @@ -62,7 +63,6 @@ let handleSearchPeople: (force?: boolean, name?: string) => Promise; let showPeople: PersonResponseDto[] = []; let countVisiblePeople: number; - let changeNameInputEl: HTMLInputElement | null; let innerHeight: number; for (const person of people) { @@ -244,8 +244,6 @@ personName = detail.name; personMerge1 = detail; edittingPerson = detail; - - setTimeout(() => changeNameInputEl?.focus(), 100); }; const handleSetBirthDate = (detail: PersonResponseDto) => { @@ -463,7 +461,7 @@ name="name" type="text" bind:value={personName} - bind:this={changeNameInputEl} + use:initInput={100} /> 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 cfcc3eeeb7..c18c4000ae 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 @@ -56,6 +56,7 @@ import { onMount } from 'svelte'; import type { PageData } from './$types'; import { listNavigation } from '$lib/actions/list-navigation'; + import { handleFavoriteAssetGrid } from '$lib/utils/asset-utils'; export let data: PageData; @@ -383,7 +384,10 @@ - assetStore.triggerUpdate()} /> + handleFavoriteAssetGrid(assets, isFavorite, assetStore)} + /> @@ -434,6 +438,7 @@
{#key refreshAssetGrid} - assetStore.triggerUpdate()} /> + handleFavoriteAssetGrid(assets, isFavorite, assetStore)} + /> {#if $selectedAssets.size > 1 || isAssetStackSelected} @@ -92,6 +96,7 @@