mirror of
https://github.com/immich-app/immich.git
synced 2025-02-11 01:18:24 -05:00
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:
parent
3058c894b1
commit
bf04d9eb39
5 changed files with 147 additions and 17 deletions
|
@ -12,7 +12,6 @@
|
||||||
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
|
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
|
||||||
import AssetSelection from './asset-selection.svelte';
|
import AssetSelection from './asset-selection.svelte';
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
import AlbumAppBar from './album-app-bar.svelte';
|
|
||||||
import UserSelectionModal from './user-selection-modal.svelte';
|
import UserSelectionModal from './user-selection-modal.svelte';
|
||||||
import ShareInfoModal from './share-info-modal.svelte';
|
import ShareInfoModal from './share-info-modal.svelte';
|
||||||
import CircleIconButton from '../shared-components/circle-icon-button.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 ContextMenu from '../shared-components/context-menu/context-menu.svelte';
|
||||||
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
||||||
import ThumbnailSelection from './thumbnail-selection.svelte';
|
import ThumbnailSelection from './thumbnail-selection.svelte';
|
||||||
|
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||||
|
|
||||||
export let album: AlbumResponseDto;
|
export let album: AlbumResponseDto;
|
||||||
|
|
||||||
|
@ -272,7 +272,7 @@
|
||||||
<section class="bg-immich-bg">
|
<section class="bg-immich-bg">
|
||||||
<!-- Multiselection mode app bar -->
|
<!-- Multiselection mode app bar -->
|
||||||
{#if isMultiSelectionMode}
|
{#if isMultiSelectionMode}
|
||||||
<AlbumAppBar
|
<ControlAppBar
|
||||||
on:close-button-click={clearMultiSelectAssetAssetHandler}
|
on:close-button-click={clearMultiSelectAssetAssetHandler}
|
||||||
backIcon={Close}
|
backIcon={Close}
|
||||||
tailwindClasses={'bg-white shadow-md'}
|
tailwindClasses={'bg-white shadow-md'}
|
||||||
|
@ -289,12 +289,12 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</AlbumAppBar>
|
</ControlAppBar>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Default app bar -->
|
<!-- Default app bar -->
|
||||||
{#if !isMultiSelectionMode}
|
{#if !isMultiSelectionMode}
|
||||||
<AlbumAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}>
|
<ControlAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}>
|
||||||
<svelte:fragment slot="trailing">
|
<svelte:fragment slot="trailing">
|
||||||
{#if album.assets.length > 0}
|
{#if album.assets.length > 0}
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
|
@ -329,7 +329,7 @@
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</AlbumAppBar>
|
</ControlAppBar>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<section class="m-auto my-[160px] w-[60%]">
|
<section class="m-auto my-[160px] w-[60%]">
|
||||||
|
|
|
@ -8,9 +8,9 @@
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
|
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
|
||||||
import { AssetResponseDto } from '@api';
|
import { AssetResponseDto } from '@api';
|
||||||
import AlbumAppBar from './album-app-bar.svelte';
|
|
||||||
import { openFileUploadDialog, UploadType } from '$lib/utils/file-uploader';
|
import { openFileUploadDialog, UploadType } from '$lib/utils/file-uploader';
|
||||||
import { albumUploadAssetStore } from '$lib/stores/album-upload-asset';
|
import { albumUploadAssetStore } from '$lib/stores/album-upload-asset';
|
||||||
|
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
@ -172,7 +172,7 @@
|
||||||
transition:fly={{ y: 500, duration: 100, easing: quintOut }}
|
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]"
|
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">
|
<svelte:fragment slot="leading">
|
||||||
{#if selectedAsset.size == 0}
|
{#if selectedAsset.size == 0}
|
||||||
<p class="text-lg">Add to album</p>
|
<p class="text-lg">Add to album</p>
|
||||||
|
@ -195,7 +195,7 @@
|
||||||
><span class="px-2">Done</span></button
|
><span class="px-2">Done</span></button
|
||||||
>
|
>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</AlbumAppBar>
|
</ControlAppBar>
|
||||||
|
|
||||||
<section class="flex flex-wrap gap-14 px-20 overflow-y-auto">
|
<section class="flex flex-wrap gap-14 px-20 overflow-y-auto">
|
||||||
{#each $assetsGroupByDate as assetsInDateGroup, groupIndex}
|
{#each $assetsGroupByDate as assetsInDateGroup, groupIndex}
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
|
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||||
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
|
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
|
||||||
import AlbumAppBar from './album-app-bar.svelte';
|
|
||||||
|
|
||||||
export let album: AlbumResponseDto;
|
export let album: AlbumResponseDto;
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
transition:fly={{ y: 500, duration: 100, easing: quintOut }}
|
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]"
|
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">
|
<svelte:fragment slot="leading">
|
||||||
<p class="text-lg">Select album cover</p>
|
<p class="text-lg">Select album cover</p>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
><span class="px-2">Done</span></button
|
><span class="px-2">Done</span></button
|
||||||
>
|
>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</AlbumAppBar>
|
</ControlAppBar>
|
||||||
|
|
||||||
<section class="flex flex-wrap gap-14 px-20 overflow-y-auto">
|
<section class="flex flex-wrap gap-14 px-20 overflow-y-auto">
|
||||||
<!-- Image grid -->
|
<!-- Image grid -->
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
import type { Load } from '@sveltejs/kit';
|
import type { Load } from '@sveltejs/kit';
|
||||||
import { setAssetInfo } from '$lib/stores/assets';
|
import { setAssetInfo } from '$lib/stores/assets';
|
||||||
|
|
||||||
export const load: Load = async ({ fetch, session }) => {
|
export const load: Load = async ({ fetch, session }) => {
|
||||||
if (!browser && !session.user) {
|
if (!browser && !session.user) {
|
||||||
return {
|
return {
|
||||||
|
@ -39,20 +40,31 @@
|
||||||
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
|
import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte';
|
||||||
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
|
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
|
||||||
import { fly } from 'svelte/transition';
|
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 ImmichThumbnail from '$lib/components/shared-components/immich-thumbnail.svelte';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
|
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
|
||||||
import { openFileUploadDialog, UploadType } from '$lib/utils/file-uploader';
|
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 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 { browser } from '$app/env';
|
||||||
|
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||||
|
|
||||||
export let user: UserResponseDto;
|
export let user: UserResponseDto;
|
||||||
|
|
||||||
let selectedGroupThumbnail: number | null;
|
let selectedGroupThumbnail: number | null;
|
||||||
let isMouseOverGroup: boolean;
|
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) {
|
$: if (isMouseOverGroup == false) {
|
||||||
selectedGroupThumbnail = null;
|
selectedGroupThumbnail = null;
|
||||||
}
|
}
|
||||||
|
@ -110,6 +122,91 @@
|
||||||
isShowAssetViewer = false;
|
isShowAssetViewer = false;
|
||||||
history.pushState(null, '', `/photos`);
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
@ -117,7 +214,28 @@
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
|
{#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)} />
|
<NavigationBar {user} on:uploadClicked={() => openFileUploadDialog(UploadType.GENERAL)} />
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
|
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
|
||||||
|
@ -136,13 +254,20 @@
|
||||||
>
|
>
|
||||||
<!-- Date group title -->
|
<!-- Date group title -->
|
||||||
<p class="font-medium text-sm text-immich-fg mb-2 flex place-items-center h-6">
|
<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
|
<div
|
||||||
in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
|
||||||
out:fly={{ x: -24, duration: 200 }}
|
out:fly={{ x: -24, duration: 200 }}
|
||||||
class="inline-block px-2 hover:cursor-pointer"
|
class="inline-block px-2 hover:cursor-pointer"
|
||||||
|
on:click={() => selectAssetGroupHandler(groupIndex)}
|
||||||
>
|
>
|
||||||
|
{#if selectedGroup.has(groupIndex)}
|
||||||
|
<CheckCircle size="24" color="#4250af" />
|
||||||
|
{:else if existingGroup.has(groupIndex)}
|
||||||
<CheckCircle size="24" color="#757575" />
|
<CheckCircle size="24" color="#757575" />
|
||||||
|
{:else}
|
||||||
|
<CircleOutline size="24" color="#757575" />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -156,7 +281,12 @@
|
||||||
<ImmichThumbnail
|
<ImmichThumbnail
|
||||||
{asset}
|
{asset}
|
||||||
on:mouseEvent={thumbnailMouseEventHandler}
|
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}
|
{groupIndex}
|
||||||
/>
|
/>
|
||||||
{/key}
|
{/key}
|
||||||
|
|
Loading…
Add table
Reference in a new issue