From e6e7f78f9ff27c9fa7796b36acdd000b1bd40b8d Mon Sep 17 00:00:00 2001
From: martabal <74269598+martabal@users.noreply.github.com>
Date: Fri, 19 Jan 2024 15:49:30 +0100
Subject: [PATCH] feat: add shortcuts to asset-grid
---
.../photos-page/actions/archive-action.svelte | 43 ++-------
.../actions/favorite-action.svelte | 43 ++-------
.../actions/select-all-assets.svelte | 25 ++---
.../components/photos-page/asset-grid.svelte | 83 ++++++++++++++---
web/src/lib/stores/asset-interaction.store.ts | 4 +
web/src/lib/utils/actions.ts | 92 ++++++++++++++++++-
.../(user)/albums/[albumId]/+page.svelte | 14 ++-
web/src/routes/(user)/archive/+page.svelte | 6 +-
web/src/routes/(user)/favorites/+page.svelte | 6 +-
.../(user)/people/[personId]/+page.svelte | 6 +-
web/src/routes/(user)/photos/+page.svelte | 4 +-
web/src/routes/(user)/search/+page.svelte | 6 +-
12 files changed, 205 insertions(+), 127 deletions(-)
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 fc3739c4e6..3daa1f423f 100644
--- a/web/src/lib/components/photos-page/actions/archive-action.svelte
+++ b/web/src/lib/components/photos-page/actions/archive-action.svelte
@@ -1,57 +1,32 @@
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 8ca73958a3..ee38082aa0 100644
--- a/web/src/lib/components/photos-page/actions/favorite-action.svelte
+++ b/web/src/lib/components/photos-page/actions/favorite-action.svelte
@@ -1,58 +1,33 @@
diff --git a/web/src/lib/components/photos-page/actions/select-all-assets.svelte b/web/src/lib/components/photos-page/actions/select-all-assets.svelte
index 1cd3e0abab..fe3e5826a7 100644
--- a/web/src/lib/components/photos-page/actions/select-all-assets.svelte
+++ b/web/src/lib/components/photos-page/actions/select-all-assets.svelte
@@ -1,10 +1,11 @@
diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte
index 53bc5440d4..7d61a0acba 100644
--- a/web/src/lib/components/photos-page/asset-grid.svelte
+++ b/web/src/lib/components/photos-page/asset-grid.svelte
@@ -2,7 +2,12 @@
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { AppRoute, AssetAction } from '$lib/constants';
- import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
+ import {
+ isAllUserOwned,
+ type AssetInteractionStore,
+ isAllFavorite,
+ isAllArchived,
+ } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { BucketPosition, type AssetStore, type Viewport } from '$lib/stores/assets.store';
import { locale, showDeleteModal } from '$lib/stores/preferences.store';
@@ -19,8 +24,11 @@
import AssetDateGroup from './asset-date-group.svelte';
import { featureFlags } from '$lib/stores/server-config.store';
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
- import { deleteAssets } from '$lib/utils/actions';
+ import { archiveAssets, deleteAssets, favoriteAssets, selectAll } from '$lib/utils/actions';
import DeleteAssetDialog from './delete-asset-dialog.svelte';
+ import { downloadArchive, downloadFile } from '$lib/utils/asset-utils';
+ import { user } from '$lib/stores/user.store';
+ import { get } from 'svelte/store';
export let isSelectionMode = false;
export let singleSelect = false;
@@ -47,6 +55,9 @@
$: idsSelectedAssets = Array.from($selectedAssets)
.filter((a) => !a.isExternal)
.map((a) => a.id);
+ $: $isAllUserOwned = Array.from($selectedAssets).every((asset) => asset.ownerId === $user.id);
+ $: $isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
+ $: $isAllArchived = Array.from($selectedAssets).every((asset) => asset.isArchived);
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>();
@@ -70,24 +81,30 @@
assetStore.disconnect();
});
- const trashOrDelete = (force: boolean = false) => {
+ const trashOrDelete = async (force: boolean = false) => {
isShowDeleteConfirmation = false;
- deleteAssets(!(isTrashEnabled && !force), (assetId) => assetStore.removeAsset(assetId), idsSelectedAssets);
- assetInteractionStore.clearMultiselect();
+ if (
+ await deleteAssets(!(isTrashEnabled && !force), (assetId) => assetStore.removeAsset(assetId), idsSelectedAssets)
+ ) {
+ assetInteractionStore.clearMultiselect();
+ }
};
- const handleKeyboardPress = (event: KeyboardEvent) => {
+ const handleKeyboardPress = async (event: KeyboardEvent) => {
if ($isSearchEnabled || shouldIgnoreShortcut(event)) {
return;
}
const key = event.key;
const shiftKey = event.shiftKey;
+ const ctrlKey = event.ctrlKey;
+ const assets = Array.from($selectedAssets);
if (!$showAssetViewer) {
switch (key) {
- case 'Escape':
- dispatch('escape');
+ case '/':
+ event.preventDefault();
+ goto(AppRoute.EXPLORE);
return;
case '?':
if (event.shiftKey) {
@@ -95,9 +112,43 @@
showShortcuts = !showShortcuts;
}
return;
- case '/':
- event.preventDefault();
- goto(AppRoute.EXPLORE);
+ case 'a':
+ case 'A':
+ if ($isMultiSelectState) {
+ if (ctrlKey) {
+ event.preventDefault();
+ selectAll(get(assetStore), assetStore, assetInteractionStore);
+ return;
+ }
+
+ if ($isAllUserOwned) {
+ if (
+ await archiveAssets(
+ !$isAllArchived,
+ (assetIds) => {
+ for (const assetId of assetIds) {
+ assetStore.removeAsset(assetId);
+ }
+ },
+ assets,
+ )
+ ) {
+ assetInteractionStore.clearMultiselect();
+ }
+ }
+ }
+ return;
+ case 'd':
+ case 'D':
+ if ($isMultiSelectState) {
+ assetInteractionStore.clearMultiselect();
+ if (assets.length === 1) {
+ await downloadFile(assets[0]);
+ return;
+ }
+
+ await downloadArchive('immich.zip', { assetIds: assets.map((asset) => asset.id) });
+ }
return;
case 'Delete':
if ($isMultiSelectState) {
@@ -113,6 +164,16 @@
trashOrDelete(force);
}
return;
+ case 'Escape':
+ dispatch('escape');
+ return;
+ case 'f':
+ if ($isMultiSelectState && $isAllUserOwned) {
+ if (await favoriteAssets(!$isAllFavorite, undefined, assets)) {
+ assetInteractionStore.clearMultiselect();
+ }
+ }
+ return;
}
}
};
diff --git a/web/src/lib/stores/asset-interaction.store.ts b/web/src/lib/stores/asset-interaction.store.ts
index 8b36861579..8a39401585 100644
--- a/web/src/lib/stores/asset-interaction.store.ts
+++ b/web/src/lib/stores/asset-interaction.store.ts
@@ -147,3 +147,7 @@ export function createAssetInteractionStore(): AssetInteractionStore {
},
};
}
+
+export const isAllUserOwned = writable();
+export const isAllFavorite = writable();
+export const isAllArchived = writable();
diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts
index 3148143c0e..bd15963ed3 100644
--- a/web/src/lib/utils/actions.ts
+++ b/web/src/lib/utils/actions.ts
@@ -1,6 +1,9 @@
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
-import { api } from '@api';
+import { api, AssetResponseDto } from '@api';
import { handleError } from './handle-error';
+import { isSelectAllCancelled, type AssetStore, BucketPosition } from '$lib/stores/assets.store';
+import { get } from 'svelte/store';
+import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store';
export type OnDelete = (assetId: string) => void;
export type OnRestore = (ids: string[]) => void;
@@ -8,7 +11,7 @@ export type OnArchive = (ids: string[], isArchived: boolean) => void;
export type OnFavorite = (ids: string[], favorite: boolean) => void;
export type OnStack = (ids: string[]) => void;
-export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]) => {
+export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids: string[]): Promise => {
try {
await api.assetApi.deleteAssets({ assetBulkDeleteDto: { ids, force } });
for (const id of ids) {
@@ -19,7 +22,92 @@ export const deleteAssets = async (force: boolean, onAssetDelete: OnDelete, ids:
message: `${force ? 'Permanently deleted' : 'Trashed'} ${ids.length} assets`,
type: NotificationType.Info,
});
+ return true;
} catch (e) {
handleError(e, 'Error deleting assets');
}
+ return false;
+};
+
+export const favoriteAssets = async (
+ isFavorite: boolean,
+ onFavorite: OnFavorite | undefined,
+ assets: AssetResponseDto[],
+): Promise => {
+ try {
+ const ids = assets.map(({ id }) => id);
+
+ if (ids.length > 0) {
+ await api.assetApi.updateAssets({ assetBulkUpdateDto: { ids, isFavorite } });
+ }
+
+ for (const asset of assets) {
+ asset.isFavorite = isFavorite;
+ }
+
+ onFavorite?.(ids, isFavorite);
+
+ notificationController.show({
+ message: isFavorite ? `Added ${ids.length} to favorites` : `Removed ${ids.length} from favorites`,
+ type: NotificationType.Info,
+ });
+
+ return true;
+ } catch (error) {
+ handleError(error, `Unable to ${isFavorite ? 'add to' : 'remove from'} favorites`);
+ }
+ return false;
+};
+
+export const archiveAssets = async (
+ isArchived: boolean,
+ onArchive: OnArchive | undefined,
+ assets: AssetResponseDto[],
+) => {
+ try {
+ const ids = assets.map(({ id }) => id);
+
+ if (ids.length > 0) {
+ await api.assetApi.updateAssets({ assetBulkUpdateDto: { ids, isArchived } });
+ }
+
+ for (const asset of assets) {
+ asset.isArchived = isArchived;
+ }
+
+ onArchive?.(ids, isArchived);
+
+ notificationController.show({
+ message: `${isArchived ? 'Archived' : 'Unarchived'} ${ids.length}`,
+ type: NotificationType.Info,
+ });
+
+ return true;
+ } catch (error) {
+ handleError(error, `Unable to ${isArchived ? 'archive' : 'unarchive'}`);
+ }
+ return false;
+};
+
+export const selectAll = async (
+ assetGridState: AssetStore,
+ assetStore: AssetStore,
+ assetInteractionStore: AssetInteractionStore,
+): Promise => {
+ isSelectAllCancelled.set(false);
+ try {
+ for (const bucket of assetGridState.buckets) {
+ if (get(isSelectAllCancelled)) {
+ break;
+ }
+ await assetStore.loadBucket(bucket.bucketDate, BucketPosition.Unknown);
+ for (const asset of bucket.assets) {
+ assetInteractionStore.selectAsset(asset);
+ }
+ }
+ return true;
+ } catch (e) {
+ handleError(e, 'Error selecting all assets');
+ }
+ return false;
};
diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte
index c878b98f6e..8547764655 100644
--- a/web/src/routes/(user)/albums/[albumId]/+page.svelte
+++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte
@@ -29,7 +29,7 @@
} from '$lib/components/shared-components/notification/notification';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AppRoute, dateFormats } from '$lib/constants';
- import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
+ import { createAssetInteractionStore, isAllUserOwned } from '$lib/stores/asset-interaction.store';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { AssetStore } from '$lib/stores/assets.store';
@@ -110,8 +110,6 @@
const { selectedAssets: timelineSelected } = timelineInteractionStore;
$: isOwned = $user.id == album.ownerId;
- $: isAllUserOwned = Array.from($selectedAssets).every((asset) => asset.ownerId === $user.id);
- $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
$: {
if (isShowActivity) {
assetGridWidth = globalWidth - (globalWidth < 768 ? 360 : 460);
@@ -440,15 +438,15 @@
- {#if isAllUserOwned}
-
+ {#if $isAllUserOwned}
+
+
{/if}
-
- {#if isOwned || isAllUserOwned}
+ {#if isOwned || $isAllUserOwned}
handleRemoveAssets(assetIds)} />
{/if}
- {#if isAllUserOwned}
+ {#if $isAllUserOwned}
assetStore.removeAsset(assetId)} />
diff --git a/web/src/routes/(user)/archive/+page.svelte b/web/src/routes/(user)/archive/+page.svelte
index 0d3714a021..293420f780 100644
--- a/web/src/routes/(user)/archive/+page.svelte
+++ b/web/src/routes/(user)/archive/+page.svelte
@@ -23,13 +23,11 @@
const assetStore = new AssetStore({ isArchived: true });
const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
-
- $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
{#if $isMultiSelectState}
assetInteractionStore.clearMultiselect()}>
- assetStore.removeAssets(ids)} />
+ assetStore.removeAssets(ids)} />
@@ -39,7 +37,7 @@
assetStore.removeAsset(assetId)} />
-
+
{/if}
diff --git a/web/src/routes/(user)/favorites/+page.svelte b/web/src/routes/(user)/favorites/+page.svelte
index 4005a5d81c..39261be23a 100644
--- a/web/src/routes/(user)/favorites/+page.svelte
+++ b/web/src/routes/(user)/favorites/+page.svelte
@@ -25,14 +25,12 @@
const assetStore = new AssetStore({ isFavorite: true });
const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
-
- $: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived);
{#if $isMultiSelectState}
assetInteractionStore.clearMultiselect()}>
- assetStore.removeAssets(ids)} />
+ assetStore.removeAssets(ids)} />
@@ -42,7 +40,7 @@
assetStore.removeAsset(assetId)} />
-
+
diff --git a/web/src/routes/(user)/people/[personId]/+page.svelte b/web/src/routes/(user)/people/[personId]/+page.svelte
index 6d7bd77865..8fb922fdb1 100644
--- a/web/src/routes/(user)/people/[personId]/+page.svelte
+++ b/web/src/routes/(user)/people/[personId]/+page.svelte
@@ -108,8 +108,6 @@
isSearchingPeople = false;
};
- $: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived);
- $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
$: $onPersonThumbnail === data.person.id &&
(thumbnailData = api.getPeopleThumbnailUrl(data.person.id) + `?now=${Date.now()}`);
@@ -394,8 +392,8 @@
$assetStore.removeAsset(assetId)} />
-
- $assetStore.removeAssets(ids)} />
+
+ $assetStore.removeAssets(ids)} />
diff --git a/web/src/routes/(user)/photos/+page.svelte b/web/src/routes/(user)/photos/+page.svelte
index 913c4d038e..0eae81c185 100644
--- a/web/src/routes/(user)/photos/+page.svelte
+++ b/web/src/routes/(user)/photos/+page.svelte
@@ -31,8 +31,6 @@
const assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
- $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
-
const handleEscape = () => {
if ($showAssetViewer) {
return;
@@ -65,7 +63,7 @@
onAssetDelete={(assetId) => assetStore.removeAsset(assetId)}
/>
-
+
assetStore.removeAssets(ids)} />
{#if $selectedAssets.size > 1}
diff --git a/web/src/routes/(user)/search/+page.svelte b/web/src/routes/(user)/search/+page.svelte
index e8bc0c08d4..f05d01db45 100644
--- a/web/src/routes/(user)/search/+page.svelte
+++ b/web/src/routes/(user)/search/+page.svelte
@@ -96,8 +96,6 @@
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);
$: searchResultAssets = data.results?.assets.items;
const onAssetDelete = (assetId: string) => {
@@ -122,8 +120,8 @@
-
-
+
+