1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

feat(web): add archive shortcut to grid (#9499)

* feat(web): add archive shortcut to grid

* Fix error

* Don't unnecessarily pass parameter

* Use an existing function to close the menu

* Deduplicate type

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Snowknight26 2024-06-06 18:23:49 -05:00 committed by GitHub
parent c6c480c882
commit 7a46f80ddc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 84 additions and 66 deletions

View file

@ -54,18 +54,7 @@
$: isOwner = $user && asset.ownerId === $user?.id; $: isOwner = $user && asset.ownerId === $user?.id;
type MenuItemEvent = type EventTypes = {
| 'addToAlbum'
| 'restoreAsset'
| 'addToSharedAlbum'
| 'asProfileImage'
| 'setAsAlbumCover'
| 'download'
| 'playSlideShow'
| 'runJob'
| 'unstack';
const dispatch = createEventDispatcher<{
back: void; back: void;
stopMotionPhoto: void; stopMotionPhoto: void;
playMotionPhoto: void; playMotionPhoto: void;
@ -83,7 +72,9 @@
playSlideShow: void; playSlideShow: void;
unstack: void; unstack: void;
showShareModal: void; showShareModal: void;
}>(); };
const dispatch = createEventDispatcher<EventTypes>();
let contextMenuPosition = { x: 0, y: 0 }; let contextMenuPosition = { x: 0, y: 0 };
let isShowAssetOptions = false; let isShowAssetOptions = false;
@ -98,7 +89,7 @@
dispatch('runJob', name); dispatch('runJob', name);
}; };
const onMenuClick = (eventName: MenuItemEvent) => { const onMenuClick = (eventName: keyof EventTypes) => {
isShowAssetOptions = false; isShowAssetOptions = false;
dispatch(eventName); dispatch(eventName);
}; };
@ -258,7 +249,7 @@
/> />
{/if} {/if}
<MenuOption <MenuOption
on:click={() => dispatch('toggleArchive')} on:click={() => onMenuClick('toggleArchive')}
icon={asset.isArchived ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline} icon={asset.isArchived ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline}
text={asset.isArchived ? $t('unarchive') : $t('archive')} text={asset.isArchived ? $t('unarchive') : $t('archive')}
/> />

View file

@ -12,7 +12,13 @@
import { stackAssetsStore } from '$lib/stores/stacked-asset.store'; import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils'; import { getAssetJobMessage, getSharedLink, handlePromiseError, isSharedLink } from '$lib/utils';
import { addAssetsToAlbum, addAssetsToNewAlbum, downloadFile, unstackAssets } from '$lib/utils/asset-utils'; import {
addAssetsToAlbum,
addAssetsToNewAlbum,
downloadFile,
unstackAssets,
toggleArchive,
} from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { shortcuts } from '$lib/actions/shortcut'; import { shortcuts } from '$lib/actions/shortcut';
import { SlideshowHistory } from '$lib/utils/slideshow-history'; import { SlideshowHistory } from '$lib/utils/slideshow-history';
@ -433,24 +439,10 @@
} }
}; };
const toggleArchive = async () => { const toggleAssetArchive = async () => {
try { const updatedAsset = await toggleArchive(asset);
const data = await updateAsset({ if (updatedAsset) {
id: asset.id, dispatch('action', { type: asset.isArchived ? AssetAction.ARCHIVE : AssetAction.UNARCHIVE, asset: asset });
updateAssetDto: {
isArchived: !asset.isArchived,
},
});
asset.isArchived = data.isArchived;
dispatch('action', { type: data.isArchived ? AssetAction.ARCHIVE : AssetAction.UNARCHIVE, asset: data });
notificationController.show({
type: NotificationType.Info,
message: asset.isArchived ? `Added to archive` : `Removed from archive`,
});
} catch (error) {
handleError(error, `Unable to ${asset.isArchived ? `add asset to` : `remove asset from`} archive`);
} }
}; };
@ -550,7 +542,7 @@
<svelte:window <svelte:window
use:shortcuts={[ use:shortcuts={[
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive }, { shortcut: { key: 'a', shift: true }, onShortcut: toggleAssetArchive },
{ shortcut: { key: 'ArrowLeft' }, onShortcut: () => navigateAsset('previous') }, { shortcut: { key: 'ArrowLeft' }, onShortcut: () => navigateAsset('previous') },
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => navigateAsset('next') }, { shortcut: { key: 'ArrowRight' }, onShortcut: () => navigateAsset('next') },
{ shortcut: { key: 'd', shift: true }, onShortcut: () => downloadFile(asset) }, { shortcut: { key: 'd', shift: true }, onShortcut: () => downloadFile(asset) },
@ -594,7 +586,7 @@
on:addToSharedAlbum={() => openAlbumPicker(true)} on:addToSharedAlbum={() => openAlbumPicker(true)}
on:playMotionPhoto={() => (shouldPlayMotionPhoto = true)} on:playMotionPhoto={() => (shouldPlayMotionPhoto = true)}
on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)} on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)}
on:toggleArchive={toggleArchive} on:toggleArchive={toggleAssetArchive}
on:asProfileImage={() => (isShowProfileImageCrop = true)} on:asProfileImage={() => (isShowProfileImageCrop = true)}
on:setAsAlbumCover={handleUpdateThumbnail} on:setAsAlbumCover={handleUpdateThumbnail}
on:runJob={({ detail: job }) => handleRunJob(job)} on:runJob={({ detail: job }) => handleRunJob(job)}

View file

@ -1,15 +1,10 @@
<script lang="ts"> <script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import {
NotificationType,
notificationController,
} from '$lib/components/shared-components/notification/notification';
import type { OnArchive } from '$lib/utils/actions'; import type { OnArchive } from '$lib/utils/actions';
import { handleError } from '$lib/utils/handle-error';
import { updateAssets } from '@immich/sdk';
import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiTimerSand } from '@mdi/js'; import { mdiArchiveArrowDownOutline, mdiArchiveArrowUpOutline, mdiTimerSand } from '@mdi/js';
import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; import MenuOption from '../../shared-components/context-menu/menu-option.svelte';
import { getAssetControlContext } from '../asset-select-control-bar.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte';
import { archiveAssets } from '$lib/utils/asset-utils';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let onArchive: OnArchive; export let onArchive: OnArchive;
@ -26,33 +21,14 @@
const handleArchive = async () => { const handleArchive = async () => {
const isArchived = !unarchive; const isArchived = !unarchive;
loading = true;
try {
const assets = [...getOwnedAssets()].filter((asset) => asset.isArchived !== isArchived); const assets = [...getOwnedAssets()].filter((asset) => asset.isArchived !== isArchived);
const ids = assets.map(({ id }) => id); loading = true;
const ids = await archiveAssets(assets, isArchived);
if (ids.length > 0) { if (ids) {
await updateAssets({ assetBulkUpdateDto: { ids, isArchived } });
}
for (const asset of assets) {
asset.isArchived = isArchived;
}
onArchive(ids, isArchived); onArchive(ids, isArchived);
notificationController.show({
message: `${isArchived ? $t('archived') : $t('unarchived')} ${ids.length}`,
type: NotificationType.Info,
});
clearSelect(); clearSelect();
} catch (error) {
handleError(error, `Unable to ${isArchived ? 'archive' : 'unarchive'}`);
} finally {
loading = false;
} }
loading = false;
}; };
</script> </script>

View file

@ -18,7 +18,7 @@
import Scrollbar from '../shared-components/scrollbar/scrollbar.svelte'; import Scrollbar from '../shared-components/scrollbar/scrollbar.svelte';
import ShowShortcuts from '../shared-components/show-shortcuts.svelte'; import ShowShortcuts from '../shared-components/show-shortcuts.svelte';
import AssetDateGroup from './asset-date-group.svelte'; import AssetDateGroup from './asset-date-group.svelte';
import { stackAssets } from '$lib/utils/asset-utils'; import { archiveAssets, stackAssets } from '$lib/utils/asset-utils';
import DeleteAssetDialog from './delete-asset-dialog.svelte'; import DeleteAssetDialog from './delete-asset-dialog.svelte';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { selectAllAssets } from '$lib/utils/asset-utils'; import { selectAllAssets } from '$lib/utils/asset-utils';
@ -48,6 +48,7 @@
$: timelineY = element?.scrollTop || 0; $: timelineY = element?.scrollTop || 0;
$: isEmpty = $assetStore.initialized && $assetStore.buckets.length === 0; $: isEmpty = $assetStore.initialized && $assetStore.buckets.length === 0;
$: idsSelectedAssets = [...$selectedAssets].map(({ id }) => id); $: idsSelectedAssets = [...$selectedAssets].map(({ id }) => id);
$: isAllArchived = [...$selectedAssets].every((asset) => asset.isArchived);
$: { $: {
if (isEmpty) { if (isEmpty) {
assetInteractionStore.clearMultiselect(); assetInteractionStore.clearMultiselect();
@ -106,6 +107,14 @@
} }
}; };
const toggleArchive = async () => {
const ids = await archiveAssets(Array.from($selectedAssets), !isAllArchived);
if (ids) {
assetStore.removeAssets(ids);
deselectAllAssets();
}
};
const focusElement = () => { const focusElement = () => {
if (document.activeElement === document.body) { if (document.activeElement === document.body) {
element.focus(); element.focus();
@ -132,6 +141,7 @@
{ shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete }, { shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() }, { shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() }, { shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
); );
} }

View file

@ -14,6 +14,7 @@ import {
getAssetInfo, getAssetInfo,
getBaseUrl, getBaseUrl,
getDownloadInfo, getDownloadInfo,
updateAsset,
updateAssets, updateAssets,
type AlbumResponseDto, type AlbumResponseDto,
type AssetResponseDto, type AssetResponseDto,
@ -23,6 +24,7 @@ import {
type UserResponseDto, type UserResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { t as translate } from 'svelte-i18n';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { handleError } from './handle-error'; import { handleError } from './handle-error';
@ -397,6 +399,53 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt
} }
}; };
export const toggleArchive = async (asset: AssetResponseDto) => {
try {
const data = await updateAsset({
id: asset.id,
updateAssetDto: {
isArchived: !asset.isArchived,
},
});
asset.isArchived = data.isArchived;
notificationController.show({
type: NotificationType.Info,
message: asset.isArchived ? `Added to archive` : `Removed from archive`,
});
} catch (error) {
handleError(error, `Unable to ${asset.isArchived ? `remove asset from` : `add asset to`} archive`);
}
return asset;
};
export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean) => {
const isArchived = archive;
const ids = assets.map(({ id }) => id);
try {
if (ids.length > 0) {
await updateAssets({ assetBulkUpdateDto: { ids, isArchived } });
}
for (const asset of assets) {
asset.isArchived = isArchived;
}
const t = get(translate);
notificationController.show({
message: `${isArchived ? t('archived') : t('unarchived')} ${ids.length}`,
type: NotificationType.Info,
});
} catch (error) {
handleError(error, `Unable to ${isArchived ? 'archive' : 'unarchive'}`);
}
return ids;
};
export const delay = async (ms: number) => { export const delay = async (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
}; };