From 5ca130f8bd4e441ff787736c3c363f6ffb4d0ff0 Mon Sep 17 00:00:00 2001 From: tanyar09 Date: Thu, 13 Nov 2025 12:58:17 -0500 Subject: [PATCH] feat: Implement favorites functionality for photos with API and UI updates This commit introduces a favorites feature for photos, allowing users to mark and manage their favorite images. The API has been updated with new endpoints for toggling favorite status, checking if a photo is a favorite, and bulk adding/removing favorites. The frontend has been enhanced to include favorite management in the PhotoViewer and Search components, with UI elements for adding/removing favorites and displaying favorite status. Documentation has been updated to reflect these changes. --- frontend/src/api/photos.ts | 33 +++- frontend/src/components/PhotoViewer.tsx | 47 ++++- frontend/src/pages/Search.tsx | 96 ++++++++- src/web/api/photos.py | 249 +++++++++++++++++++++++- src/web/app.py | 2 +- src/web/db/models.py | 20 ++ src/web/schemas/photos.py | 32 ++- src/web/schemas/search.py | 1 + src/web/services/search_service.py | 35 ++++ 9 files changed, 507 insertions(+), 8 deletions(-) diff --git a/frontend/src/api/photos.ts b/frontend/src/api/photos.ts index f3143c0..ebbe18c 100644 --- a/frontend/src/api/photos.ts +++ b/frontend/src/api/photos.ts @@ -73,7 +73,7 @@ export const photosApi = { }, searchPhotos: async (params: { - search_type: 'name' | 'date' | 'tags' | 'no_faces' | 'no_tags' | 'processed' | 'unprocessed' + search_type: 'name' | 'date' | 'tags' | 'no_faces' | 'no_tags' | 'processed' | 'unprocessed' | 'favorites' person_name?: string tag_names?: string match_all?: boolean @@ -89,6 +89,36 @@ export const photosApi = { return data }, + toggleFavorite: async (photoId: number): Promise<{ photo_id: number; is_favorite: boolean; message: string }> => { + const { data } = await apiClient.post( + `/api/v1/photos/${photoId}/toggle-favorite` + ) + return data + }, + + checkFavorite: async (photoId: number): Promise<{ photo_id: number; is_favorite: boolean }> => { + const { data } = await apiClient.get( + `/api/v1/photos/${photoId}/is-favorite` + ) + return data + }, + + bulkAddFavorites: async (photoIds: number[]): Promise<{ message: string; added_count: number; already_favorite_count: number; total_requested: number }> => { + const { data } = await apiClient.post( + '/api/v1/photos/bulk-add-favorites', + { photo_ids: photoIds } + ) + return data + }, + + bulkRemoveFavorites: async (photoIds: number[]): Promise<{ message: string; removed_count: number; not_favorite_count: number; total_requested: number }> => { + const { data } = await apiClient.post( + '/api/v1/photos/bulk-remove-favorites', + { photo_ids: photoIds } + ) + return data + }, + openFolder: async (photoId: number): Promise<{ message: string; folder: string }> => { const { data } = await apiClient.post<{ message: string; folder: string }>( `/api/v1/photos/${photoId}/open-folder` @@ -115,6 +145,7 @@ export interface PhotoSearchResult { tags: string[] has_faces: boolean face_count: number + is_favorite?: boolean } export interface SearchPhotosResponse { diff --git a/frontend/src/components/PhotoViewer.tsx b/frontend/src/components/PhotoViewer.tsx index df185f7..1eec0ac 100644 --- a/frontend/src/components/PhotoViewer.tsx +++ b/frontend/src/components/PhotoViewer.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, useRef } from 'react' -import { PhotoSearchResult } from '../api/photos' +import { PhotoSearchResult, photosApi } from '../api/photos' import { apiClient } from '../api/client' interface PhotoViewerProps { @@ -37,6 +37,10 @@ export default function PhotoViewer({ photos, initialIndex, onClose }: PhotoView const [isPlaying, setIsPlaying] = useState(false) const [slideshowInterval, setSlideshowInterval] = useState(3) // seconds const slideshowTimerRef = useRef(null) + + // Favorite state + const [isFavorite, setIsFavorite] = useState(false) + const [loadingFavorite, setLoadingFavorite] = useState(false) const currentPhoto = photos[currentIndex] const canGoPrev = currentIndex > 0 @@ -180,10 +184,34 @@ export default function PhotoViewer({ photos, initialIndex, onClose }: PhotoView setPanX(0) setPanY(0) + // Load favorite status when photo changes + photosApi.checkFavorite(currentPhoto.id) + .then(result => setIsFavorite(result.is_favorite)) + .catch(err => { + console.error('Error checking favorite:', err) + setIsFavorite(false) + }) + // Preload adjacent images when current photo changes preloadAdjacent(currentIndex) }, [currentIndex, currentPhoto, photos.length]) + // Toggle favorite + const toggleFavorite = async () => { + if (loadingFavorite || !currentPhoto) return + + setLoadingFavorite(true) + try { + const result = await photosApi.toggleFavorite(currentPhoto.id) + setIsFavorite(result.is_favorite) + } catch (error) { + console.error('Error toggling favorite:', error) + alert('Error updating favorite status') + } finally { + setLoadingFavorite(false) + } + } + // Keyboard navigation useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -255,9 +283,24 @@ export default function PhotoViewer({ photos, initialIndex, onClose }: PhotoView - {/* Top Right Play Button */} + {/* Top Right Controls */}
+ {/* Favorite button */} + + + {/* Slideshow controls */} {isPlaying && (