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}