feat(web): enable the shortcut "f" to favorite/unfavorite in multiselection mode (#31411)

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
(cherry picked from commit aeee9be333f4faac877b99cacd355329a5001c25)
This commit is contained in:
Bautrell12
2026-10-29 20:43:01 +03:00
committed by Codex
parent f2e05e9d98
commit c76bd08f73
3 changed files with 46 additions and 25 deletions
@@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import MenuOption from '#lib/components/shared-components/context-menu/MenuOption.svelte'; import MenuOption from '#lib/components/shared-components/context-menu/MenuOption.svelte';
import { assetMultiSelectManager } from '#lib/managers/asset-multi-select-manager.svelte.ts'; import { assetMultiSelectManager } from '#lib/managers/asset-multi-select-manager.svelte.ts';
import { toggleFavoriteAssets } from '#lib/services/asset.service.ts';
import type { OnFavorite } from '#lib/utils/actions.ts'; import type { OnFavorite } from '#lib/utils/actions.ts';
import { handleError } from '#lib/utils/handle-error.ts'; import { IconButton } from '@immich/ui';
import { updateAssets } from '@immich/sdk';
import { IconButton, toastManager } from '@immich/ui';
import { mdiHeartMinusOutline, mdiHeartOutline, mdiTimerSand } from '@mdi/js'; import { mdiHeartMinusOutline, mdiHeartOutline, mdiTimerSand } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@@ -27,28 +26,8 @@
try { try {
const assets = assetMultiSelectManager.ownedAssets.filter((asset) => asset.isFavorite !== isFavorite); const assets = assetMultiSelectManager.ownedAssets.filter((asset) => asset.isFavorite !== isFavorite);
await toggleFavoriteAssets(assets, isFavorite, onFavorite);
const ids = assets.map(({ id }) => id);
if (ids.length > 0) {
await updateAssets({ assetBulkUpdateDto: { ids, isFavorite } });
}
for (const asset of assets) {
asset.isFavorite = isFavorite;
}
onFavorite?.(ids, isFavorite);
toastManager.primary(
isFavorite
? $t('added_to_favorites_count', { values: { count: ids.length } })
: $t('removed_from_favorites_count', { values: { count: ids.length } }),
);
assetMultiSelectManager.clear(); assetMultiSelectManager.clear();
} catch (error) {
handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: isFavorite } }));
} finally { } finally {
loading = false; loading = false;
} }
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
import { shortcuts, type ShortcutOptions } from '#lib/actions/shortcut.ts'; import { shortcuts, type ShortcutOptions } from '#lib/actions/shortcut.ts';
import { import {
setFocusToAsset as setFocusAssetInit, setFocusToAsset as setFocusAssetInit,
@@ -14,6 +13,7 @@
import NavigateToDateModal from '#lib/modals/NavigateToDateModal.svelte'; import NavigateToDateModal from '#lib/modals/NavigateToDateModal.svelte';
import ShortcutsModal from '#lib/modals/ShortcutsModal.svelte'; import ShortcutsModal from '#lib/modals/ShortcutsModal.svelte';
import { Route } from '#lib/route.ts'; import { Route } from '#lib/route.ts';
import { toggleFavoriteAssets } from '#lib/services/asset.service.ts';
import { handleStack } from '#lib/services/stack.service.ts'; import { handleStack } from '#lib/services/stack.service.ts';
import { keyboardManager } from '#lib/stores/keyboard-manager.svelte.ts'; import { keyboardManager } from '#lib/stores/keyboard-manager.svelte.ts';
import { showDeleteModal } from '#lib/stores/preferences.store.ts'; import { showDeleteModal } from '#lib/stores/preferences.store.ts';
@@ -21,6 +21,7 @@
import { handlePromiseError } from '#lib/utils.ts'; import { handlePromiseError } from '#lib/utils.ts';
import { deleteAssets } from '#lib/utils/actions.ts'; import { deleteAssets } from '#lib/utils/actions.ts';
import { selectAllAssets } from '#lib/utils/asset-utils.ts'; import { selectAllAssets } from '#lib/utils/asset-utils.ts';
import { goto } from '$app/navigation';
import { isModalOpen, modalManager } from '@immich/ui'; import { isModalOpen, modalManager } from '@immich/ui';
type Props = { type Props = {
@@ -62,6 +63,14 @@
onEscape?.(); onEscape?.();
}; };
const toggleFavoriteSelected = async () => {
const allAssets = assetInteraction.ownedAssets;
const isFavorite = allAssets.some((a) => !a.isFavorite);
const assetsToUpdate = allAssets.filter((asset) => asset.isFavorite !== isFavorite);
await toggleFavoriteAssets(assetsToUpdate, isFavorite);
assetInteraction.clear();
};
const onSelectStart = (e: Event) => { const onSelectStart = (e: Event) => {
if (!searchStore.isSearchEnabled && assetInteraction.selectionActive && keyboardManager.shift) { if (!searchStore.isSearchEnabled && assetInteraction.selectionActive && keyboardManager.shift) {
e.preventDefault(); e.preventDefault();
@@ -125,6 +134,7 @@
{ shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) }, { shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) },
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => assetInteraction.clear() }, { shortcut: { key: 'D', ctrl: true }, onShortcut: () => assetInteraction.clear() },
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() }, { shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
{ shortcut: { key: 'f' }, onShortcut: () => toggleFavoriteSelected() },
); );
} else { } else {
// conflicting shortcuts // conflicting shortcuts
+32
View File
@@ -15,6 +15,7 @@ import { Route } from '#lib/route.ts';
import { SlideshowState, slideshowStore } from '#lib/stores/slideshow.store.ts'; import { SlideshowState, slideshowStore } from '#lib/stores/slideshow.store.ts';
import { getAssetMediaUrl, getSharedLink, sleep } from '#lib/utils.ts'; import { getAssetMediaUrl, getSharedLink, sleep } from '#lib/utils.ts';
import { downloadUrl } from '#lib/utils.ts'; import { downloadUrl } from '#lib/utils.ts';
import type { OnFavorite } from '#lib/utils/actions.ts';
import { handleError } from '#lib/utils/handle-error.ts'; import { handleError } from '#lib/utils/handle-error.ts';
import { getFormatter } from '#lib/utils/i18n.ts'; import { getFormatter } from '#lib/utils/i18n.ts';
import { import {
@@ -63,6 +64,8 @@ import {
mdiTune, mdiTune,
} from '@mdi/js'; } from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n'; import type { MessageFormatter } from 'svelte-i18n';
import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
export const getAssetBulkActions = ($t: MessageFormatter, album?: AlbumResponseDto) => { export const getAssetBulkActions = ($t: MessageFormatter, album?: AlbumResponseDto) => {
@@ -496,6 +499,35 @@ const handleUnfavorite = async (asset: AssetResponseDto) => {
} }
}; };
export const toggleFavoriteAssets = async (
assets: Pick<AssetResponseDto, 'id' | 'isFavorite'>[],
isFavorite: boolean,
onFavorite?: OnFavorite,
) => {
const ids = assets.map((a) => a.id);
const $t = get(t);
try {
if (ids.length > 0) {
await updateAssets({ assetBulkUpdateDto: { ids, isFavorite } });
}
for (const asset of assets) {
asset.isFavorite = isFavorite;
}
onFavorite?.(ids, isFavorite);
toastManager.primary(
isFavorite
? $t('added_to_favorites_count', { values: { count: ids.length } })
: $t('removed_from_favorites_count', { values: { count: ids.length } }),
);
} catch (error) {
handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: isFavorite } }));
}
};
const handleRate = async (asset: AssetResponseDto, rating: number) => { const handleRate = async (asset: AssetResponseDto, rating: number) => {
const $t = await getFormatter(); const $t = await getFormatter();