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:
parent
c6c480c882
commit
7a46f80ddc
5 changed files with 84 additions and 66 deletions
|
@ -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')}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue