From 6f6f847ee2c13a1c79fe86dbb0dc3d9fd3bb427f Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 21 Apr 2023 10:10:08 -0500 Subject: [PATCH] feat(web): Add action button to search result page (#2303) * feat(web): Add action button to search result page * fix test * rename * pr feedback * better condition * fix test --- .../assets/thumbnail/thumbnail.svelte | 7 + .../gallery-viewer/gallery-viewer.svelte | 2 + web/src/routes/(user)/search/+page.svelte | 261 +++++++++++++++++- 3 files changed, 262 insertions(+), 8 deletions(-) diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index 39a17faa19..edf93707c4 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -7,6 +7,7 @@ import MotionPauseOutline from 'svelte-material-icons/MotionPauseOutline.svelte'; import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte'; import Star from 'svelte-material-icons/Star.svelte'; + import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte'; import ImageThumbnail from './image-thumbnail.svelte'; import VideoThumbnail from './video-thumbnail.svelte'; @@ -22,6 +23,7 @@ export let disabled = false; export let readonly = false; export let publicSharedKey: string | undefined = undefined; + export let showArchiveIcon = false; let mouseOver = false; @@ -114,6 +116,11 @@ {/if} + {#if showArchiveIcon && asset.isArchived} +
+ +
+ {/if} = new Set(); export let disableAssetSelect = false; export let viewFrom: ViewFrom; + export let showArchiveIcon = false; let isShowAssetViewer = false; @@ -141,6 +142,7 @@ on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))} on:select={selectAssetHandler} selected={selectedAssets.has(asset)} + {showArchiveIcon} /> {/each} diff --git a/web/src/routes/(user)/search/+page.svelte b/web/src/routes/(user)/search/+page.svelte index f8ea502a65..308cc9a1d6 100644 --- a/web/src/routes/(user)/search/+page.svelte +++ b/web/src/routes/(user)/search/+page.svelte @@ -7,7 +7,25 @@ import ImageOffOutline from 'svelte-material-icons/ImageOffOutline.svelte'; import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte'; import { afterNavigate, goto } from '$app/navigation'; - + import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte'; + import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.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 CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte'; + import { + notificationController, + NotificationType + } from '$lib/components/shared-components/notification/notification'; + import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils'; + import { AlbumResponseDto, api, AssetResponseDto, SharedLinkType } from '@api'; + import Close from 'svelte-material-icons/Close.svelte'; + import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte'; + import ArchiveArrowUpOutline from 'svelte-material-icons/ArchiveArrowUpOutline.svelte'; + import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte'; + import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; + import Plus from 'svelte-material-icons/Plus.svelte'; + import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; + import { locale } from '$lib/stores/preferences.store'; export let data: PageData; // The GalleryViewer pushes it's own history state, which causes weird @@ -23,14 +41,209 @@ }); $: term = $page.url.searchParams.get('q') || data.term || ''; + + let selectedAssets: Set = new Set(); + $: isMultiSelectionMode = selectedAssets.size > 0; + $: isAllArchived = Array.from(selectedAssets).every((asset) => asset.isArchived); + $: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite); + + let contextMenuPosition = { x: 0, y: 0 }; + let isShowCreateSharedLinkModal = false; + let isShowAddMenu = false; + let isShowAlbumPicker = false; + let addToSharedAlbum = false; + $: searchResultAssets = data.results.assets.items; + + const handleShowMenu = ({ x, y }: MouseEvent) => { + contextMenuPosition = { x, y }; + isShowAddMenu = !isShowAddMenu; + }; + + const handleShowAlbumPicker = (shared: boolean) => { + isShowAddMenu = false; + isShowAlbumPicker = true; + addToSharedAlbum = shared; + }; + + const handleAddToNewAlbum = (event: CustomEvent) => { + isShowAlbumPicker = false; + + const { albumName }: { albumName: string } = event.detail; + const assetIds = Array.from(selectedAssets).map((asset) => asset.id); + api.albumApi.createAlbum({ albumName, assetIds }).then((response) => { + const { id, albumName } = response.data; + + notificationController.show({ + message: `Added ${assetIds.length} to ${albumName}`, + type: NotificationType.Info + }); + + clearMultiSelectAssetAssetHandler(); + + goto('/albums/' + id); + }); + }; + + const handleAddToAlbum = async (event: CustomEvent<{ album: AlbumResponseDto }>) => { + isShowAlbumPicker = false; + const album = event.detail.album; + + const assetIds = Array.from(selectedAssets).map((asset) => asset.id); + + addAssetsToAlbum(album.id, assetIds).then(() => { + clearMultiSelectAssetAssetHandler(); + }); + }; + + const handleDownloadFiles = async () => { + await bulkDownload('immich', Array.from(selectedAssets), () => { + clearMultiSelectAssetAssetHandler(); + }); + }; + + const toggleArchive = async () => { + let cnt = 0; + for (const asset of selectedAssets) { + api.assetApi.updateAsset(asset.id, { + isArchived: !isAllArchived + }); + cnt = cnt + 1; + + asset.isArchived = !isAllArchived; + + searchResultAssets = searchResultAssets.map((a: AssetResponseDto) => { + if (a.id === asset.id) { + a = asset; + } + + return a; + }); + } + + notificationController.show({ + message: `${isAllArchived ? `Remove ${cnt} from` : `Add ${cnt} to`} archive`, + type: NotificationType.Info + }); + + clearMultiSelectAssetAssetHandler(); + }; + + const toggleFavorite = () => { + isShowAddMenu = false; + + let cnt = 0; + for (const asset of selectedAssets) { + api.assetApi.updateAsset(asset.id, { + isFavorite: !isAllFavorite + }); + cnt = cnt + 1; + + asset.isFavorite = !isAllFavorite; + + searchResultAssets = searchResultAssets.map((a: AssetResponseDto) => { + if (a.id === asset.id) { + a = asset; + } + return a; + }); + } + + notificationController.show({ + message: `${isAllFavorite ? `Remove ${cnt} from` : `Add ${cnt} to`} favorites`, + type: NotificationType.Info + }); + + clearMultiSelectAssetAssetHandler(); + }; + + const clearMultiSelectAssetAssetHandler = () => { + selectedAssets = new Set(); + }; + + const deleteSelectedAssetHandler = async () => { + try { + if ( + window.confirm( + `Caution! Are you sure you want to delete ${selectedAssets.size} assets? This step also deletes assets in the album(s) to which they belong. You can not undo this action!` + ) + ) { + const { data: deletedAssets } = await api.assetApi.deleteAsset({ + ids: Array.from(selectedAssets).map((a) => a.id) + }); + + for (const asset of deletedAssets) { + if (asset.status == 'SUCCESS') { + searchResultAssets = searchResultAssets.filter( + (a: AssetResponseDto) => a.id != asset.id + ); + } + } + + clearMultiSelectAssetAssetHandler(); + } + } catch (e) { + notificationController.show({ + type: NotificationType.Error, + message: 'Error deleting assets, check console for more details' + }); + console.error('Error deleteSelectedAssetHandler', e); + } + }; + const handleCreateSharedLink = async () => { + isShowCreateSharedLinkModal = true; + }; + + const handleCloseSharedLinkModal = () => { + clearMultiSelectAssetAssetHandler(); + isShowCreateSharedLinkModal = false; + };
- goto(previousRoute)} backIcon={ArrowLeft}> -
- -
-
+ {#if isMultiSelectionMode} + + +

+ Selected {selectedAssets.size.toLocaleString($locale)} +

+
+ + + + + + + + + +
+ {:else} + goto(previousRoute)} backIcon={ArrowLeft}> +
+ +
+
+ {/if}
@@ -39,9 +252,10 @@ {#if data.results?.assets?.items.length > 0}
{:else} @@ -57,4 +271,35 @@ {/if}
+ + {#if isShowAddMenu} + (isShowAddMenu = false)}> +
+ + handleShowAlbumPicker(false)} text="Add to Album" /> + handleShowAlbumPicker(true)} text="Add to Shared Album" /> +
+
+ {/if} + + {#if isShowAlbumPicker} + (isShowAlbumPicker = false)} + /> + {/if} + + {#if isShowCreateSharedLinkModal} + + {/if}