Jelajahi Sumber

feat(web): add Favorites page (#1397)

* Duplicate photos page and rename to favorites

* Implement basic functionality to page

* Sort imports

* Add missing sharing code

* Remove unused import

* Fix formatting

* Use GalleryViewer and new api endpoint

* Merge useFavorites into page

* Run prettier

* Move favorites in side-bar

* Remove favorites when unfavorited

* Fix close shared link model

* Add favorite count to side-bar

* Add add to favorites option

* Fix formatting

* Add favorite icon to image thumbnails

* Change var to let
James 2 tahun lalu
induk
melakukan
de0e218440

+ 11 - 10
web/src/lib/components/asset-viewer/asset-viewer.svelte

@@ -1,26 +1,26 @@
 <script lang="ts">
 <script lang="ts">
-	import { createEventDispatcher, onMount, onDestroy } from 'svelte';
-	import { fly } from 'svelte/transition';
-	import AssetViewerNavBar from './asset-viewer-nav-bar.svelte';
-	import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
-	import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
-	import PhotoViewer from './photo-viewer.svelte';
-	import DetailPanel from './detail-panel.svelte';
 	import { goto } from '$app/navigation';
 	import { goto } from '$app/navigation';
 	import { downloadAssets } from '$lib/stores/download';
 	import { downloadAssets } from '$lib/stores/download';
-	import VideoViewer from './video-viewer.svelte';
-	import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte';
 	import {
 	import {
+		AlbumResponseDto,
 		api,
 		api,
 		AssetResponseDto,
 		AssetResponseDto,
 		AssetTypeEnum,
 		AssetTypeEnum,
-		AlbumResponseDto,
 		SharedLinkResponseDto
 		SharedLinkResponseDto
 	} from '@api';
 	} from '@api';
+	import { createEventDispatcher, onDestroy, onMount } from 'svelte';
+	import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
+	import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
+	import { fly } from 'svelte/transition';
+	import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte';
 	import {
 	import {
 		notificationController,
 		notificationController,
 		NotificationType
 		NotificationType
 	} from '../shared-components/notification/notification';
 	} from '../shared-components/notification/notification';
+	import AssetViewerNavBar from './asset-viewer-nav-bar.svelte';
+	import DetailPanel from './detail-panel.svelte';
+	import PhotoViewer from './photo-viewer.svelte';
+	import VideoViewer from './video-viewer.svelte';
 
 
 	import { assetStore } from '$lib/stores/assets.store';
 	import { assetStore } from '$lib/stores/assets.store';
 	import { addAssetsToAlbum } from '$lib/utils/asset-utils';
 	import { addAssetsToAlbum } from '$lib/utils/asset-utils';
@@ -217,6 +217,7 @@
 		});
 		});
 
 
 		asset.isFavorite = data.isFavorite;
 		asset.isFavorite = data.isFavorite;
+		assetStore.updateAsset(asset.id, data.isFavorite);
 	};
 	};
 
 
 	const openAlbumPicker = (shared: boolean) => {
 	const openAlbumPicker = (shared: boolean) => {

+ 14 - 7
web/src/lib/components/shared-components/immich-thumbnail.svelte

@@ -1,14 +1,15 @@
 <script lang="ts">
 <script lang="ts">
-	import { createEventDispatcher, onDestroy } from 'svelte';
-	import { fade, fly } from 'svelte/transition';
 	import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
 	import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
+	import { AssetResponseDto, AssetTypeEnum, getFileUrl, ThumbnailFormat } from '@api';
+	import { createEventDispatcher, onDestroy } from 'svelte';
 	import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
 	import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
-	import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
-	import PauseCircleOutline from 'svelte-material-icons/PauseCircleOutline.svelte';
-	import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
 	import MotionPauseOutline from 'svelte-material-icons/MotionPauseOutline.svelte';
 	import MotionPauseOutline from 'svelte-material-icons/MotionPauseOutline.svelte';
+	import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
+	import PauseCircleOutline from 'svelte-material-icons/PauseCircleOutline.svelte';
+	import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
+	import Star from 'svelte-material-icons/Star.svelte';
+	import { fade, fly } from 'svelte/transition';
 	import LoadingSpinner from './loading-spinner.svelte';
 	import LoadingSpinner from './loading-spinner.svelte';
-	import { AssetResponseDto, AssetTypeEnum, getFileUrl, ThumbnailFormat } from '@api';
 
 
 	const dispatch = createEventDispatcher();
 	const dispatch = createEventDispatcher();
 
 
@@ -163,7 +164,7 @@
 		{#if mouseOver || selected || disabled}
 		{#if mouseOver || selected || disabled}
 			<div
 			<div
 				in:fade={{ duration: 200 }}
 				in:fade={{ duration: 200 }}
-				class={`w-full ${getOverlaySelectorIconStyle()} via-white/0 to-white/0 absolute p-2  z-10`}
+				class={`w-full ${getOverlaySelectorIconStyle()} via-white/0 to-white/0 absolute p-2 z-10`}
 			>
 			>
 				<button
 				<button
 					on:click={onIconClickedHandler}
 					on:click={onIconClickedHandler}
@@ -182,6 +183,12 @@
 			</div>
 			</div>
 		{/if}
 		{/if}
 
 
+		{#if asset.isFavorite}
+			<div class="w-full absolute bottom-2 left-2 z-10">
+				<Star size="24" color={'white'} />
+			</div>
+		{/if}
+
 		<!-- Playback and info -->
 		<!-- Playback and info -->
 		{#if asset.type === AssetTypeEnum.Video}
 		{#if asset.type === AssetTypeEnum.Video}
 			<div
 			<div

+ 51 - 5
web/src/lib/components/shared-components/side-bar/side-bar.svelte

@@ -1,18 +1,20 @@
 <script lang="ts">
 <script lang="ts">
 	import { page } from '$app/stores';
 	import { page } from '$app/stores';
+	import { api } from '@api';
+	import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
 	import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
 	import ImageAlbum from 'svelte-material-icons/ImageAlbum.svelte';
 	import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
 	import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
-	import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte';
 	import InformationOutline from 'svelte-material-icons/InformationOutline.svelte';
 	import InformationOutline from 'svelte-material-icons/InformationOutline.svelte';
-	import SideBarButton from './side-bar-button.svelte';
-	import StatusBox from '../status-box.svelte';
-	import { api } from '@api';
+	import StarOutline from 'svelte-material-icons/StarOutline.svelte';
 	import { fade } from 'svelte/transition';
 	import { fade } from 'svelte/transition';
-	import LoadingSpinner from '../loading-spinner.svelte';
 	import { AppRoute } from '../../../constants';
 	import { AppRoute } from '../../../constants';
+	import LoadingSpinner from '../loading-spinner.svelte';
+	import StatusBox from '../status-box.svelte';
+	import SideBarButton from './side-bar-button.svelte';
 
 
 	let showAssetCount = false;
 	let showAssetCount = false;
 	let showSharingCount = false;
 	let showSharingCount = false;
+	let showFavoritesCount = false;
 	let showAlbumsCount = false;
 	let showAlbumsCount = false;
 
 
 	const getAssetCount = async () => {
 	const getAssetCount = async () => {
@@ -24,6 +26,14 @@
 		};
 		};
 	};
 	};
 
 
+	const getFavoriteCount = async () => {
+		const { data: assets } = await api.assetApi.getAllAssets(true);
+
+		return {
+			favorites: assets.length
+		};
+	};
+
 	const getAlbumCount = async () => {
 	const getAlbumCount = async () => {
 		const { data: albumCount } = await api.albumApi.getAlbumCountByUserId();
 		const { data: albumCount } = await api.albumApi.getAlbumCountByUserId();
 		return {
 		return {
@@ -104,6 +114,42 @@
 	<div class="text-xs ml-5 my-4 dark:text-immich-dark-fg">
 	<div class="text-xs ml-5 my-4 dark:text-immich-dark-fg">
 		<p>LIBRARY</p>
 		<p>LIBRARY</p>
 	</div>
 	</div>
+	<a
+		data-sveltekit-preload-data="hover"
+		href={AppRoute.FAVORITES}
+		class="relative"
+		draggable="false"
+	>
+		<SideBarButton
+			title="Favorites"
+			logo={StarOutline}
+			isSelected={$page.route.id == AppRoute.FAVORITES}
+		/>
+
+		<div
+			id="favorite-count-info"
+			class="absolute right-4 top-[15px] z-40 text-xs hover:cursor-help"
+			on:mouseenter={() => (showFavoritesCount = true)}
+			on:mouseleave={() => (showFavoritesCount = false)}
+		>
+			<InformationOutline size={18} color="#989a9f" />
+			{#if showFavoritesCount}
+				<div
+					transition:fade={{ duration: 200 }}
+					id="asset-count-info-detail"
+					class="w-32 rounded-lg p-4 shadow-lg bg-white absolute -right-[135px] top-0 z-[9999] flex place-items-center place-content-center"
+				>
+					{#await getFavoriteCount()}
+						<LoadingSpinner />
+					{:then data}
+						<div>
+							<p>{data.favorites} Favorites</p>
+						</div>
+					{/await}
+				</div>
+			{/if}
+		</div>
+	</a>
 	<a data-sveltekit-preload-data="hover" href={AppRoute.ALBUMS} class="relative" draggable="false">
 	<a data-sveltekit-preload-data="hover" href={AppRoute.ALBUMS} class="relative" draggable="false">
 		<SideBarButton
 		<SideBarButton
 			title="Albums"
 			title="Albums"

+ 1 - 0
web/src/lib/constants.ts

@@ -8,6 +8,7 @@ export enum AppRoute {
 	ADMIN_JOBS = '/admin/jobs-status',
 	ADMIN_JOBS = '/admin/jobs-status',
 
 
 	ALBUMS = '/albums',
 	ALBUMS = '/albums',
+	FAVORITES = '/favorites',
 	PHOTOS = '/photos',
 	PHOTOS = '/photos',
 	SHARING = '/sharing'
 	SHARING = '/sharing'
 }
 }

+ 16 - 4
web/src/lib/stores/assets.store.ts

@@ -1,8 +1,8 @@
-import { writable } from 'svelte/store';
-import lodash from 'lodash-es';
-import { api, AssetCountByTimeBucketResponseDto } from '@api';
 import { AssetGridState } from '$lib/models/asset-grid-state';
 import { AssetGridState } from '$lib/models/asset-grid-state';
 import { calculateViewportHeightByNumberOfAsset } from '$lib/utils/viewport-utils';
 import { calculateViewportHeightByNumberOfAsset } from '$lib/utils/viewport-utils';
+import { api, AssetCountByTimeBucketResponseDto } from '@api';
+import lodash from 'lodash-es';
+import { writable } from 'svelte/store';
 
 
 /**
 /**
  * The state that holds information about the asset grid
  * The state that holds information about the asset grid
@@ -141,12 +141,24 @@ function createAssetStore() {
 		});
 		});
 	};
 	};
 
 
+	const updateAsset = (assetId: string, isFavorite: boolean) => {
+		assetGridState.update((state) => {
+			const bucketIndex = state.buckets.findIndex((b) => b.assets.some((a) => a.id === assetId));
+			const assetIndex = state.buckets[bucketIndex].assets.findIndex((a) => a.id === assetId);
+			state.buckets[bucketIndex].assets[assetIndex].isFavorite = isFavorite;
+
+			state.assets = lodash.flatMap(state.buckets, (b) => b.assets);
+			return state;
+		});
+	};
+
 	return {
 	return {
 		setInitialState,
 		setInitialState,
 		getAssetsByBucket,
 		getAssetsByBucket,
 		removeAsset,
 		removeAsset,
 		updateBucketHeight,
 		updateBucketHeight,
-		cancelBucketRequest
+		cancelBucketRequest,
+		updateAsset
 	};
 	};
 }
 }
 
 

+ 21 - 0
web/src/routes/favorites/+page.server.ts

@@ -0,0 +1,21 @@
+import type { PageServerLoad } from './$types';
+import { redirect, error } from '@sveltejs/kit';
+
+export const load: PageServerLoad = async ({ parent }) => {
+	try {
+		const { user } = await parent();
+		if (!user) {
+			throw error(400, 'Not logged in');
+		}
+
+		return {
+			user,
+			meta: {
+				title: 'Favorites'
+			}
+		};
+	} catch (e) {
+		console.log('Photo page load error', e);
+		throw redirect(302, '/auth/login');
+	}
+};

+ 140 - 0
web/src/routes/favorites/+page.svelte

@@ -0,0 +1,140 @@
+<script lang="ts">
+	import CircleIconButton from '$lib/components/shared-components/circle-icon-button.svelte';
+	import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
+	import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
+	import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
+	import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
+	import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
+	import { handleError } from '$lib/utils/handle-error';
+	import { api, AssetResponseDto, SharedLinkType } from '@api';
+	import { onMount } from 'svelte';
+	import Close from 'svelte-material-icons/Close.svelte';
+	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
+	import StarMinusOutline from 'svelte-material-icons/StarMinusOutline.svelte';
+	import Error from '../+error.svelte';
+	import type { PageData } from './$types';
+
+	export let data: PageData;
+
+	let favorites: AssetResponseDto[] = [];
+	let isShowCreateSharedLinkModal = false;
+	let selectedAssets: Set<AssetResponseDto> = new Set();
+
+	$: isMultiSelectionMode = selectedAssets.size > 0;
+
+	onMount(async () => {
+		try {
+			const { data: assets } = await api.assetApi.getAllAssets(true);
+			favorites = assets;
+		} catch {
+			handleError(Error, 'Unable to load favorites');
+		}
+	});
+
+	const clearMultiSelectAssetAssetHandler = () => {
+		selectedAssets = new Set();
+	};
+
+	const handleCreateSharedLink = async () => {
+		isShowCreateSharedLinkModal = true;
+	};
+
+	const handleCloseSharedLinkModal = () => {
+		clearMultiSelectAssetAssetHandler();
+		isShowCreateSharedLinkModal = false;
+	};
+
+	const handleRemoveFavorite = async () => {
+		for (const asset of selectedAssets) {
+			try {
+				await api.assetApi.updateAsset(asset.id, {
+					isFavorite: false
+				});
+				favorites = favorites.filter((a) => a.id != asset.id);
+			} catch {
+				handleError(Error, 'Error updating asset favorite state');
+			}
+		}
+
+		clearMultiSelectAssetAssetHandler();
+	};
+</script>
+
+<section>
+	<NavigationBar user={data.user} shouldShowUploadButton={false} />
+</section>
+
+<section
+	class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg  dark:bg-immich-dark-bg"
+>
+	<SideBar />
+
+	<!-- Multiselection mode app bar -->
+	{#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 dark:text-immich-dark-primary">
+					Selected {selectedAssets.size}
+				</p>
+			</svelte:fragment>
+			<svelte:fragment slot="trailing">
+				<CircleIconButton
+					title="Share"
+					logo={ShareVariantOutline}
+					on:click={handleCreateSharedLink}
+				/>
+				<CircleIconButton
+					title="Remove Favorite"
+					logo={StarMinusOutline}
+					on:click={handleRemoveFavorite}
+				/>
+			</svelte:fragment>
+		</ControlAppBar>
+	{/if}
+
+	<!-- Create shared link modal -->
+	{#if isShowCreateSharedLinkModal}
+		<CreateSharedLinkModal
+			sharedAssets={Array.from(selectedAssets)}
+			shareType={SharedLinkType.Individual}
+			on:close={handleCloseSharedLinkModal}
+		/>
+	{/if}
+
+	<!-- Main Section -->
+	<section class="overflow-y-auto relative immich-scrollbar">
+		<section
+			id="favorite-content"
+			class="relative pt-8 pl-4 mb-12 bg-immich-bg dark:bg-immich-dark-bg"
+		>
+			<div class="px-4 flex justify-between place-items-center dark:text-immich-dark-fg">
+				<div>
+					<p class="font-medium">Favorites</p>
+				</div>
+			</div>
+
+			<div class="my-4">
+				<hr class="dark:border-immich-dark-gray" />
+			</div>
+
+			<!-- Empty Message -->
+			{#if favorites.length === 0}
+				<div
+					class="border dark:border-immich-dark-gray hover:bg-immich-primary/5 dark:hover:bg-immich-dark-primary/25 hover:cursor-pointer p-5 w-[50%] m-auto mt-10 bg-gray-50 dark:bg-immich-dark-gray rounded-3xl flex flex-col place-content-center place-items-center"
+				>
+					<img src="/empty-1.svg" alt="Empty shared album" width="500" draggable="false" />
+
+					<p class="text-center text-immich-text-gray-500 dark:text-immich-dark-fg">
+						Add favorites to quickly find your best pictures and videos
+					</p>
+				</div>
+			{/if}
+
+			<GalleryViewer assets={favorites} bind:selectedAssets />
+		</section>
+	</section>
+</section>

+ 14 - 0
web/src/routes/favorites/[assetId]/+page.server.ts

@@ -0,0 +1,14 @@
+import { redirect } from '@sveltejs/kit';
+export const prerender = false;
+
+import type { PageServerLoad } from './$types';
+
+export const load: PageServerLoad = async ({ parent }) => {
+	const { user } = await parent();
+
+	if (!user) {
+		throw redirect(302, '/auth/login');
+	} else {
+		throw redirect(302, '/favorites');
+	}
+};

+ 0 - 0
web/src/routes/favorites/[assetId]/+page.svelte


+ 40 - 17
web/src/routes/photos/+page.svelte

@@ -1,33 +1,33 @@
 <script lang="ts">
 <script lang="ts">
-	import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
-	import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
+	import { goto } from '$app/navigation';
 	import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
 	import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
+	import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte';
+	import CircleIconButton from '$lib/components/shared-components/circle-icon-button.svelte';
 	import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
 	import ContextMenu from '$lib/components/shared-components/context-menu/context-menu.svelte';
 	import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
 	import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
-	import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte';
-	import { goto } from '$app/navigation';
-	import type { PageData } from './$types';
-	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
-	import { openFileUploadDialog } from '$lib/utils/file-uploader';
+	import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
+	import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
+	import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
+	import {
+		notificationController,
+		NotificationType
+	} from '$lib/components/shared-components/notification/notification';
+	import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
 	import {
 	import {
 		assetInteractionStore,
 		assetInteractionStore,
 		isMultiSelectStoreState,
 		isMultiSelectStoreState,
 		selectedAssets
 		selectedAssets
 	} from '$lib/stores/asset-interaction.store';
 	} from '$lib/stores/asset-interaction.store';
-	import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
+	import { assetStore } from '$lib/stores/assets.store';
+	import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils';
+	import { openFileUploadDialog } from '$lib/utils/file-uploader';
+	import { AlbumResponseDto, api, SharedLinkType } from '@api';
 	import Close from 'svelte-material-icons/Close.svelte';
 	import Close from 'svelte-material-icons/Close.svelte';
 	import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
 	import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
-	import CircleIconButton from '$lib/components/shared-components/circle-icon-button.svelte';
 	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
 	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
 	import Plus from 'svelte-material-icons/Plus.svelte';
 	import Plus from 'svelte-material-icons/Plus.svelte';
-	import { AlbumResponseDto, api, SharedLinkType } from '@api';
-	import {
-		notificationController,
-		NotificationType
-	} from '$lib/components/shared-components/notification/notification';
-	import { assetStore } from '$lib/stores/assets.store';
-	import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils';
-	import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
+	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
+	import type { PageData } from './$types';
 
 
 	export let data: PageData;
 	export let data: PageData;
 	let isShowCreateSharedLinkModal = false;
 	let isShowCreateSharedLinkModal = false;
@@ -73,6 +73,28 @@
 		isShowAddMenu = !isShowAddMenu;
 		isShowAddMenu = !isShowAddMenu;
 	};
 	};
 
 
+	const handleAddToFavorites = () => {
+		isShowAddMenu = false;
+
+		let cnt = 0;
+		for (const asset of $selectedAssets) {
+			if (!asset.isFavorite) {
+				api.assetApi.updateAsset(asset.id, {
+					isFavorite: true
+				});
+				assetStore.updateAsset(asset.id, true);
+				cnt = cnt + 1;
+			}
+		}
+
+		notificationController.show({
+			message: `Added ${cnt} to favorites`,
+			type: NotificationType.Info
+		});
+
+		assetInteractionStore.clearMultiselect();
+	};
+
 	const handleShowAlbumPicker = (shared: boolean) => {
 	const handleShowAlbumPicker = (shared: boolean) => {
 		isShowAddMenu = false;
 		isShowAddMenu = false;
 		isShowAlbumPicker = true;
 		isShowAlbumPicker = true;
@@ -163,6 +185,7 @@
 	{#if isShowAddMenu}
 	{#if isShowAddMenu}
 		<ContextMenu {...contextMenuPosition} on:clickoutside={() => (isShowAddMenu = false)}>
 		<ContextMenu {...contextMenuPosition} on:clickoutside={() => (isShowAddMenu = false)}>
 			<div class="flex flex-col rounded-lg ">
 			<div class="flex flex-col rounded-lg ">
+				<MenuOption on:click={handleAddToFavorites} text="Add to Favorites" />
 				<MenuOption on:click={() => handleShowAlbumPicker(false)} text="Add to Album" />
 				<MenuOption on:click={() => handleShowAlbumPicker(false)} text="Add to Album" />
 				<MenuOption on:click={() => handleShowAlbumPicker(true)} text="Add to Shared Album" />
 				<MenuOption on:click={() => handleShowAlbumPicker(true)} text="Add to Shared Album" />
 			</div>
 			</div>