Feature - Delete asset on the web (#436)

* Added selection mechanism to photos page

* Added control app bar

* Refactor AlbumAppBar into ControlAppBar

* Added addtional micro interactions when in multi selection mode

* Implemented delete selected asset and rerender
This commit is contained in:
Alex 2022-08-08 22:06:11 -05:00 committed by GitHub
parent 3058c894b1
commit bf04d9eb39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 147 additions and 17 deletions

View file

@ -12,7 +12,6 @@
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
import AssetSelection from './asset-selection.svelte';
import _ from 'lodash-es';
import AlbumAppBar from './album-app-bar.svelte';
import UserSelectionModal from './user-selection-modal.svelte';
import ShareInfoModal from './share-info-modal.svelte';
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
@ -22,6 +21,7 @@
import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
import ThumbnailSelection from './thumbnail-selection.svelte';
import ControlAppBar from '../shared-components/control-app-bar.svelte';
export let album: AlbumResponseDto;
@ -272,7 +272,7 @@
<section class="bg-immich-bg">
<!-- Multiselection mode app bar -->
{#if isMultiSelectionMode}
<AlbumAppBar
<ControlAppBar
on:close-button-click={clearMultiSelectAssetAssetHandler}
backIcon={Close}
tailwindClasses={'bg-white shadow-md'}
@ -289,12 +289,12 @@
/>
{/if}
</svelte:fragment>
</AlbumAppBar>
</ControlAppBar>
{/if}
<!-- Default app bar -->
{#if !isMultiSelectionMode}
<AlbumAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}>
<ControlAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}>
<svelte:fragment slot="trailing">
{#if album.assets.length > 0}
<CircleIconButton
@ -329,7 +329,7 @@
>
{/if}
</svelte:fragment>
</AlbumAppBar>
</ControlAppBar>
{/if}
<section class="m-auto my-[160px] w-[60%]">

View file

@ -8,9 +8,9 @@
import moment from 'moment';
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
import { AssetResponseDto } from '@api';
import AlbumAppBar from './album-app-bar.svelte';
import { openFileUploadDialog, UploadType } from '$lib/utils/file-uploader';
import { albumUploadAssetStore } from '$lib/stores/album-upload-asset';
import ControlAppBar from '../shared-components/control-app-bar.svelte';
const dispatch = createEventDispatcher();
@ -172,7 +172,7 @@
transition:fly={{ y: 500, duration: 100, easing: quintOut }}
class="absolute top-0 left-0 w-full h-full py-[160px] bg-immich-bg z-[9999]"
>
<AlbumAppBar on:close-button-click={() => dispatch('go-back')}>
<ControlAppBar on:close-button-click={() => dispatch('go-back')}>
<svelte:fragment slot="leading">
{#if selectedAsset.size == 0}
<p class="text-lg">Add to album</p>
@ -195,7 +195,7 @@
><span class="px-2">Done</span></button
>
</svelte:fragment>
</AlbumAppBar>
</ControlAppBar>
<section class="flex flex-wrap gap-14 px-20 overflow-y-auto">
{#each $assetsGroupByDate as assetsInDateGroup, groupIndex}

View file

@ -3,8 +3,8 @@
import { createEventDispatcher } from 'svelte';
import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
import ControlAppBar from '../shared-components/control-app-bar.svelte';
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
import AlbumAppBar from './album-app-bar.svelte';
export let album: AlbumResponseDto;
@ -24,7 +24,7 @@
transition:fly={{ y: 500, duration: 100, easing: quintOut }}
class="absolute top-0 left-0 w-full h-full py-[160px] bg-immich-bg z-[9999]"
>
<AlbumAppBar on:close-button-click={() => dispatch('close')}>
<ControlAppBar on:close-button-click={() => dispatch('close')}>
<svelte:fragment slot="leading">
<p class="text-lg">Select album cover</p>
</svelte:fragment>
@ -37,7 +37,7 @@
><span class="px-2">Done</span></button
>
</svelte:fragment>
</AlbumAppBar>
</ControlAppBar>
<section class="flex flex-wrap gap-14 px-20 overflow-y-auto">
<!-- Image grid -->

View file

@ -3,6 +3,7 @@
import type { Load } from '@sveltejs/kit';
import { setAssetInfo } from '$lib/stores/assets';
export const load: Load = async ({ fetch, session }) => {
if (!browser && !session.user) {
return {
@ -39,20 +40,31 @@
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
import { fly } from 'svelte/transition';
import { assetsGroupByDate, flattenAssetGroupByDate } from '$lib/stores/assets';
import { assetsGroupByDate, flattenAssetGroupByDate, assets } from '$lib/stores/assets';
import ImmichThumbnail from '$lib/components/shared-components/immich-thumbnail.svelte';
import moment from 'moment';
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
import { openFileUploadDialog, UploadType } from '$lib/utils/file-uploader';
import { AssetResponseDto, UserResponseDto } from '@api';
import { api, AssetResponseDto, UserResponseDto } from '@api';
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
import CircleOutline from 'svelte-material-icons/CircleOutline.svelte';
import CircleIconButton from '$lib/components/shared-components/circle-icon-button.svelte';
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
import Close from 'svelte-material-icons/Close.svelte';
import { browser } from '$app/env';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
export let user: UserResponseDto;
let selectedGroupThumbnail: number | null;
let isMouseOverGroup: boolean;
let multiSelectedAssets = new Set<AssetResponseDto>();
$: isMultiSelectionMode = multiSelectedAssets.size > 0;
let selectedGroup: Set<number> = new Set();
let existingGroup: Set<number> = new Set();
$: if (isMouseOverGroup == false) {
selectedGroupThumbnail = null;
}
@ -110,6 +122,91 @@
isShowAssetViewer = false;
history.pushState(null, '', `/photos`);
};
const selectAssetHandler = (asset: AssetResponseDto, groupIndex: number) => {
let temp = new Set(multiSelectedAssets);
if (multiSelectedAssets.has(asset)) {
temp.delete(asset);
const tempSelectedGroup = new Set(selectedGroup);
tempSelectedGroup.delete(groupIndex);
selectedGroup = tempSelectedGroup;
} else {
temp.add(asset);
}
multiSelectedAssets = temp;
// Check if all assets are selected in a group to toggle the group selection's icon
if (!selectedGroup.has(groupIndex)) {
const assetsInGroup = $assetsGroupByDate[groupIndex];
let selectedAssetsInGroupCount = 0;
assetsInGroup.forEach((asset) => {
if (multiSelectedAssets.has(asset)) {
selectedAssetsInGroupCount++;
}
});
// if all assets are selected in a group, add the group to selected group
if (selectedAssetsInGroupCount == assetsInGroup.length) {
selectedGroup = selectedGroup.add(groupIndex);
}
}
};
const clearMultiSelectAssetAssetHandler = () => {
multiSelectedAssets = new Set();
selectedGroup = new Set();
existingGroup = new Set();
};
const selectAssetGroupHandler = (groupIndex: number) => {
if (existingGroup.has(groupIndex)) return;
let tempSelectedGroup = new Set(selectedGroup);
let tempSelectedAsset = new Set(multiSelectedAssets);
if (selectedGroup.has(groupIndex)) {
tempSelectedGroup.delete(groupIndex);
tempSelectedAsset.forEach((asset) => {
if ($assetsGroupByDate[groupIndex].find((a) => a.id == asset.id)) {
tempSelectedAsset.delete(asset);
}
});
} else {
tempSelectedGroup.add(groupIndex);
tempSelectedAsset = new Set([...multiSelectedAssets, ...$assetsGroupByDate[groupIndex]]);
}
multiSelectedAssets = tempSelectedAsset;
selectedGroup = tempSelectedGroup;
};
const deleteSelectedAssetHandler = async () => {
try {
if (
window.confirm(
`Are you sure you want to delete ${multiSelectedAssets.size} assets? This action cannot be undone.`
)
) {
const { data: deletedAssets } = await api.assetApi.deleteAsset({
ids: Array.from(multiSelectedAssets).map((a) => a.id)
});
for (const asset of deletedAssets) {
if (asset.status == 'SUCCESS') {
$assets = $assets.filter((a) => a.id !== asset.id);
}
}
clearMultiSelectAssetAssetHandler();
}
} catch (e) {
console.log('Error deleteSelectedAssetHandler', e);
}
};
</script>
<svelte:head>
@ -117,7 +214,28 @@
</svelte:head>
<section>
<NavigationBar {user} on:uploadClicked={() => openFileUploadDialog(UploadType.GENERAL)} />
{#if isMultiSelectionMode}
<ControlAppBar
on:close-button-click={clearMultiSelectAssetAssetHandler}
backIcon={Close}
tailwindClasses={'bg-white shadow-md'}
>
<svelte:fragment slot="leading">
<p class="font-medium text-immich-primary">Selected {multiSelectedAssets.size}</p>
</svelte:fragment>
<svelte:fragment slot="trailing">
<CircleIconButton
title="Delete"
logo={DeleteOutline}
on:click={deleteSelectedAssetHandler}
/>
</svelte:fragment>
</ControlAppBar>
{/if}
{#if !isMultiSelectionMode}
<NavigationBar {user} on:uploadClicked={() => openFileUploadDialog(UploadType.GENERAL)} />
{/if}
</section>
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
@ -136,13 +254,20 @@
>
<!-- Date group title -->
<p class="font-medium text-sm text-immich-fg mb-2 flex place-items-center h-6">
{#if selectedGroupThumbnail === groupIndex && isMouseOverGroup}
{#if (selectedGroupThumbnail === groupIndex && isMouseOverGroup) || isMultiSelectionMode}
<div
in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
out:fly={{ x: -24, duration: 200 }}
class="inline-block px-2 hover:cursor-pointer"
on:click={() => selectAssetGroupHandler(groupIndex)}
>
<CheckCircle size="24" color="#757575" />
{#if selectedGroup.has(groupIndex)}
<CheckCircle size="24" color="#4250af" />
{:else if existingGroup.has(groupIndex)}
<CheckCircle size="24" color="#757575" />
{:else}
<CircleOutline size="24" color="#757575" />
{/if}
</div>
{/if}
@ -156,7 +281,12 @@
<ImmichThumbnail
{asset}
on:mouseEvent={thumbnailMouseEventHandler}
on:click={viewAssetHandler}
on:click={(event) =>
isMultiSelectionMode
? selectAssetHandler(asset, groupIndex)
: viewAssetHandler(event)}
on:select={() => selectAssetHandler(asset, groupIndex)}
selected={multiSelectedAssets.has(asset)}
{groupIndex}
/>
{/key}