From add11012b4415678ea38df616e6a99ba32643d78 Mon Sep 17 00:00:00 2001 From: Rahul Harpal Date: Sat, 4 Jan 2025 21:51:54 +0530 Subject: [PATCH 1/8] Added Multiple folder support and bug fixes --- backend/app/routes/images.py | 184 +++++++++++++----- frontend/api/api-functions/images.ts | 20 +- frontend/api/apiEndpoints.ts | 1 + frontend/package.json | 4 +- frontend/src-tauri/src/services/mod.rs | 136 ++++++------- .../src/components/AITagging/AIgallery.tsx | 106 +++++----- .../components/AITagging/FilterControls.tsx | 4 +- frontend/src/components/Album/Album.tsx | 31 +-- frontend/src/components/Album/AlbumCard.tsx | 4 +- frontend/src/components/Album/AlbumForm.tsx | 23 ++- frontend/src/components/Album/Albumview.tsx | 4 +- .../src/components/Album/ImageSelection.tsx | 4 +- .../FolderPicker/AITaggingFolderPicker.tsx | 48 +++++ .../FolderPicker/DeleteSelectedImagePage.tsx | 42 ++-- .../components/FolderPicker/FolderPicker.tsx | 46 +---- .../src/components/Media/MediaGallery.tsx | 34 +++- frontend/src/components/Media/MediaView.tsx | 8 +- .../src/components/Media/SortningControls.tsx | 2 +- .../components/Navigation/Navbar/Navbar.tsx | 11 +- .../components/Navigation/Sidebar/Sidebar.tsx | 2 +- frontend/src/components/ui/Icons/Icons.tsx | 176 +---------------- .../src/controllers/InitialPageController.tsx | 31 +++ frontend/src/controllers/SetupController.tsx | 32 --- .../Setup/{Setup.tsx => SetupScreen.tsx} | 12 +- frontend/src/hooks/LocalStorage.ts | 14 +- frontend/src/hooks/UseVideos.ts | 12 +- frontend/src/hooks/folderService.ts | 9 +- frontend/src/hooks/useFolderPath.ts | 6 +- frontend/src/hooks/useImages.ts | 117 +++++------ frontend/src/pages/Dashboard/Dashboard.tsx | 6 +- frontend/src/pages/SettingsPage/Settings.tsx | 106 ++++++++-- frontend/src/pages/Setupscreen/Setup.tsx | 8 +- frontend/src/pages/VideosPage/Videos.tsx | 5 +- frontend/src/types/Album.ts | 3 +- frontend/src/types/Media.ts | 2 +- 35 files changed, 634 insertions(+), 619 deletions(-) create mode 100644 frontend/src/components/FolderPicker/AITaggingFolderPicker.tsx create mode 100644 frontend/src/controllers/InitialPageController.tsx delete mode 100644 frontend/src/controllers/SetupController.tsx rename frontend/src/features/Setup/{Setup.tsx => SetupScreen.tsx} (61%) diff --git a/backend/app/routes/images.py b/backend/app/routes/images.py index 48fc9e3b..16b5194d 100644 --- a/backend/app/routes/images.py +++ b/backend/app/routes/images.py @@ -255,28 +255,26 @@ def delete_multiple_images(payload: dict): parts.insert(parts.index("images") + 1, "PictoPy.thumbnails") thumb_nail_image_path = os.sep.join(parts) - - if os.path.exists(path) : - try : + if os.path.exists(path): + try: os.remove(path) except PermissionError: print(f"Permission denied for file '{thumb_nail_image_path}'.") except Exception as e: print(f"An error occurred: {e}") - + else: print(f"File '{path}' does not exist.") - - if os.path.exists(thumb_nail_image_path) : - try : + + if os.path.exists(thumb_nail_image_path): + try: os.remove(thumb_nail_image_path) except PermissionError: print(f"Permission denied for file '{thumb_nail_image_path}'.") except Exception as e: print(f"An error occurred: {e}") - else : + else: print(f"File '{thumb_nail_image_path}' does not exist.") - delete_image_db(path) deleted_paths.append(path) @@ -479,6 +477,98 @@ async def add_folder(payload: dict): @router.post("/generate-thumbnails") @exception_handler_wrapper def generate_thumbnails(payload: dict): + if "folder_paths" not in payload or not isinstance(payload["folder_paths"], list): + return JSONResponse( + status_code=400, + content={ + "status_code": 400, + "content": { + "success": False, + "error": "Invalid or missing 'folder_paths' in payload", + "message": "'folder_paths' must be a list of folder paths", + }, + }, + ) + + folder_paths = payload["folder_paths"] + image_extensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".webp"] + failed_paths = [] + + for folder_path in folder_paths: + if not os.path.isdir(folder_path): + failed_paths.append( + { + "folder_path": folder_path, + "error": "Invalid folder path", + "message": "The provided path is not a valid directory", + } + ) + continue + + for root, _, files in os.walk(folder_path): + # Do not generate thumbnails for the "PictoPy.thumbnails" folder + if "PictoPy.thumbnails" in root: + continue + + # Create the "PictoPy.thumbnails" folder in the current directory (`root`) + thumbnail_folder = os.path.join(root, "PictoPy.thumbnails") + os.makedirs(thumbnail_folder, exist_ok=True) + + for file in files: + file_path = os.path.join(root, file) + file_extension = os.path.splitext(file_path)[1].lower() + if file_extension in image_extensions: + try: + # Create a unique thumbnail name based on the file name + thumbnail_name = file + thumbnail_path = os.path.join(thumbnail_folder, thumbnail_name) + + # Skip if the thumbnail already exists + if os.path.exists(thumbnail_path): + continue + + # Generate the thumbnail + img = Image.open(file_path) + img.thumbnail((400, 400)) + img.save(thumbnail_path) + except Exception as e: + failed_paths.append( + { + "folder_path": folder_path, + "file": file_path, + "error": "Thumbnail generation error", + "message": f"Error processing file {file}: {str(e)}", + } + ) + + if failed_paths: + return JSONResponse( + status_code=207, # Multi-Status (some succeeded, some failed) + content={ + "status_code": 207, + "content": { + "success": False, + "error": "Partial processing", + "message": "Some folders or files could not be processed", + "failed_paths": failed_paths, + }, + }, + ) + + return JSONResponse( + status_code=201, + content={ + "data": "", + "message": "Thumbnails generated successfully for all valid folders", + "success": True, + }, + ) + + +# Delete all the thumbnails present in the given folder +@router.delete("/delete-thumbnails") +@exception_handler_wrapper +def delete_thumbnails(payload: dict): if "folder_path" not in payload: return JSONResponse( status_code=400, @@ -506,53 +596,47 @@ def generate_thumbnails(payload: dict): }, ) - thumbnail_folder = os.path.join(folder_path, "PictoPy.thumbnails") - os.makedirs(thumbnail_folder, exist_ok=True) + # List to store any errors encountered while deleting thumbnails + failed_deletions = [] - image_extensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".webp"] - for root, _, files in os.walk(folder_path): - if "PictoPy.thumbnails" in root: - continue - for file in files: - file_path = os.path.join(root, file) - file_extension = os.path.splitext(file_path)[1].lower() - if file_extension in image_extensions: + # Walk through the folder path and find all `PictoPy.thumbnails` folders + for root, dirs, _ in os.walk(folder_path): + for dir_name in dirs: + if dir_name == "PictoPy.thumbnails": + thumbnail_folder = os.path.join(root, dir_name) try: - # Create a unique name based on the relative folder structure - relative_path = os.path.relpath(root, folder_path) - sanitized_relative_path = relative_path.replace( - os.sep, "." - ) # Replace path separators - thumbnail_name = ( - f"{sanitized_relative_path}.{file}" - if relative_path != "." - else file - ) - thumbnail_path = os.path.join(thumbnail_folder, thumbnail_name) - if os.path.exists(thumbnail_path): - # print(f"Thumbnail {thumbnail_name} already exists. Skipping.") - continue - img = Image.open(file_path) - img.thumbnail((400, 400)) - img.save(thumbnail_path) + # Delete the thumbnail folder + shutil.rmtree(thumbnail_folder) + print(f"Deleted: {thumbnail_folder}") except Exception as e: - return JSONResponse( - status_code=500, - content={ - "status_code": 500, - "content": { - "success": False, - "error": "Internal server error", - "message": f"Error processing file {file}: {str(e)}", - }, - }, + failed_deletions.append( + { + "folder": thumbnail_folder, + "error": str(e), + } ) + if failed_deletions: + return JSONResponse( + status_code=500, + content={ + "status_code": 500, + "content": { + "success": False, + "error": "Some thumbnail folders could not be deleted", + "message": "See the `failed_deletions` field for details.", + "failed_deletions": failed_deletions, + }, + }, + ) + return JSONResponse( - status_code=201, + status_code=200, content={ - "data": "", - "message": "Thumbnails generated successfully", - "success": True, + "status_code": 200, + "content": { + "success": True, + "message": "All PictoPy.thumbnails folders have been successfully deleted.", + }, }, ) diff --git a/frontend/api/api-functions/images.ts b/frontend/api/api-functions/images.ts index 783dc465..80c8f2e7 100644 --- a/frontend/api/api-functions/images.ts +++ b/frontend/api/api-functions/images.ts @@ -31,11 +31,9 @@ const parseAndSortImageData = (data: APIResponse['data']): Image[] => { const parsedImages: Image[] = Object.entries(data.images).map( ([src, tags]) => { const url = convertFileSrc(src); - const thumbnailUrl = convertFileSrc( - extractThumbnailPath(data.folder_path, src), - ); + const thumbnailUrl = convertFileSrc(extractThumbnailPath(src)); return { - imagePath:src, + imagePath: src, title: src.substring(src.lastIndexOf('\\') + 1), thumbnailUrl, url, @@ -85,12 +83,24 @@ export const addMultImages = async (paths: string[]) => { return data; }; -export const generateThumbnails = async (folderPath: string) => { +export const generateThumbnails = async (folderPath: string[]) => { const response = await fetch(imagesEndpoints.generateThumbnails, { method: 'POST', headers: { 'Content-Type': 'application/json', }, + body: JSON.stringify({ folder_paths: folderPath }), + }); + const data = await response.json(); + return data; +}; + +export const deleteThumbnails = async (folderPath: string) => { + const response = await fetch(imagesEndpoints.deleteThumbnails, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + }, body: JSON.stringify({ folder_path: folderPath }), }); const data = await response.json(); diff --git a/frontend/api/apiEndpoints.ts b/frontend/api/apiEndpoints.ts index 5949e95c..c5603feb 100644 --- a/frontend/api/apiEndpoints.ts +++ b/frontend/api/apiEndpoints.ts @@ -7,6 +7,7 @@ export const imagesEndpoints = { addFolder: `${BACKED_URL}/images/add-folder`, addMultipleImages: `${BACKED_URL}/images/multiple-images`, generateThumbnails: `${BACKED_URL}/images/generate-thumbnails`, + deleteThumbnails: `${BACKED_URL}/images/delete-thumbnails`, }; export const albumEndpoints = { diff --git a/frontend/package.json b/frontend/package.json index b9084747..ab02743d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,5 +1,5 @@ { - "name": "tauri-app", + "name": "PictoPy", "private": true, "version": "0.0.0", "type": "module", @@ -54,4 +54,4 @@ "vite": "^5.0.0", "vite-plugin-eslint": "^1.8.1" } -} +} \ No newline at end of file diff --git a/frontend/src-tauri/src/services/mod.rs b/frontend/src-tauri/src/services/mod.rs index 068dac2e..cf3e66d4 100644 --- a/frontend/src-tauri/src/services/mod.rs +++ b/frontend/src-tauri/src/services/mod.rs @@ -35,7 +35,7 @@ pub fn get_images_in_folder( pub fn get_all_images_with_cache( state: tauri::State, cache_state: tauri::State, - directory: &str, + directories: Vec, ) -> Result>>, String> { let cached_images = cache_state.get_cached_images(); @@ -60,35 +60,35 @@ pub fn get_all_images_with_cache( } map } else { - let all_images = state.get_all_images(directory); let mut map: HashMap>> = HashMap::new(); + let mut all_image_paths: Vec = Vec::new(); - for path in all_images { - if let Ok(metadata) = std::fs::metadata(&path) { - let date = metadata - .created() - .or_else(|_| metadata.modified()) - .unwrap_or_else(|_| SystemTime::now()); + for directory in directories { + let all_images = state.get_all_images(&directory); - let datetime: DateTime = date.into(); - let year = datetime.year() as u32; - let month = datetime.month(); - map.entry(year) - .or_insert_with(HashMap::new) - .entry(month) - .or_insert_with(Vec::new) - .push(path.to_str().unwrap_or_default().to_string()); + for path in all_images { + if let Ok(metadata) = std::fs::metadata(&path) { + let date = metadata + .created() + .or_else(|_| metadata.modified()) + .unwrap_or_else(|_| SystemTime::now()); + + let datetime: DateTime = date.into(); + let year = datetime.year() as u32; + let month = datetime.month(); + map.entry(year) + .or_insert_with(HashMap::new) + .entry(month) + .or_insert_with(Vec::new) + .push(path.to_str().unwrap_or_default().to_string()); + + all_image_paths.push(path); // Collect all paths for caching + } } } // Cache the flattened list of image paths - let flattened: Vec = map - .values() - .flat_map(|year_map| year_map.values()) - .flatten() - .map(|s| PathBuf::from(s)) - .collect(); - if let Err(e) = cache_state.cache_images(&flattened) { + if let Err(e) = cache_state.cache_images(&all_image_paths) { eprintln!("Failed to cache images: {}", e); } @@ -109,59 +109,63 @@ pub fn get_all_images_with_cache( pub fn get_all_videos_with_cache( state: tauri::State, cache_state: tauri::State, - directory: &str, + directories: Vec, // Updated to take an array of directories ) -> Result>>, String> { let cached_videos = cache_state.get_cached_videos(); - let mut videos_by_year_month = if let Some(cached) = cached_videos { - let mut map: HashMap>> = HashMap::new(); - for path in cached { - if let Ok(metadata) = std::fs::metadata(&path) { - if let Ok(created) = metadata.created() { - let datetime: DateTime = created.into(); - let year = datetime.year() as u32; - let month = datetime.month(); - map.entry(year) - .or_insert_with(HashMap::new) - .entry(month) - .or_insert_with(Vec::new) - .push(path.to_str().unwrap_or_default().to_string()); + let mut videos_by_year_month: HashMap>> = + if let Some(cached) = cached_videos { + let mut map: HashMap>> = HashMap::new(); + for path in cached { + if let Ok(metadata) = std::fs::metadata(&path) { + if let Ok(created) = metadata.created() { + let datetime: DateTime = created.into(); + let year = datetime.year() as u32; + let month = datetime.month(); + map.entry(year) + .or_insert_with(HashMap::new) + .entry(month) + .or_insert_with(Vec::new) + .push(path.to_str().unwrap_or_default().to_string()); + } } } - } - map - } else { - let all_videos = state.get_all_videos(directory); - let mut map: HashMap>> = HashMap::new(); - - for path in all_videos { - if let Ok(metadata) = std::fs::metadata(&path) { - if let Ok(created) = metadata.created() { - let datetime: DateTime = created.into(); - let year = datetime.year() as u32; - let month = datetime.month(); - map.entry(year) - .or_insert_with(HashMap::new) - .entry(month) - .or_insert_with(Vec::new) - .push(path.to_str().unwrap_or_default().to_string()); + map + } else { + let mut map: HashMap>> = HashMap::new(); + for directory in directories { + let all_videos = state.get_all_videos(&directory); + for path in all_videos { + if let Ok(metadata) = std::fs::metadata(&path) { + if let Ok(created) = metadata.created() { + let datetime: DateTime = created.into(); + let year = datetime.year() as u32; + let month = datetime.month(); + map.entry(year) + .or_insert_with(HashMap::new) + .entry(month) + .or_insert_with(Vec::new) + .push(path.to_str().unwrap_or_default().to_string()); + } + } } } - } - let flattened: Vec = map - .values() - .flat_map(|year_map| year_map.values()) - .flatten() - .map(|s| PathBuf::from(s)) - .collect(); - if let Err(e) = cache_state.cache_videos(&flattened) { - eprintln!("Failed to cache videos: {}", e); - } + // Cache the aggregated video paths + let flattened: Vec = map + .values() + .flat_map(|year_map| year_map.values()) + .flatten() + .map(|s| PathBuf::from(s)) + .collect(); + if let Err(e) = cache_state.cache_videos(&flattened) { + eprintln!("Failed to cache videos: {}", e); + } - map - }; + map + }; + // Sort the videos within each month for year_map in videos_by_year_month.values_mut() { for month_vec in year_map.values_mut() { month_vec.sort(); diff --git a/frontend/src/components/AITagging/AIgallery.tsx b/frontend/src/components/AITagging/AIgallery.tsx index 45e3fe9d..62aa488c 100644 --- a/frontend/src/components/AITagging/AIgallery.tsx +++ b/frontend/src/components/AITagging/AIgallery.tsx @@ -28,16 +28,15 @@ export default function AIGallery({ type: 'image' | 'video'; folderPath: string; }) { - const { successData, isLoading: loading } = usePictoQuery({ + const { successData, isLoading: isGeneratingTags } = usePictoQuery({ queryFn: async () => await getAllImageObjects(), queryKey: ['ai-tagging-images', 'ai'], }); - const { mutate: generateThumbnail, isPending: isCreating } = usePictoMutation( - { + const { mutate: generateThumbnailAPI, isPending: isGeneratingThumbnails } = + usePictoMutation({ mutationFn: generateThumbnails, autoInvalidateTags: ['ai-tagging-images', 'ai'], - }, - ); + }); let mediaItems = successData ?? []; const [filterTag, setFilterTag] = useState(''); const [currentPage, setCurrentPage] = useState(1); @@ -52,13 +51,12 @@ export default function AIGallery({ ); const filteredMediaItems = useMemo(() => { return filterTag - ? mediaItems.filter((mediaItem: any) => - mediaItem.tags.includes(filterTag), - ) - : mediaItems; -}, [filterTag, mediaItems, loading]); -const [pageNo,setpageNo] = useState(20); - + ? mediaItems.filter((mediaItem: any) => + mediaItem.tags.includes(filterTag), + ) + : mediaItems; + }, [filterTag, mediaItems, isGeneratingTags]); + const [pageNo, setpageNo] = useState(20); const currentItems = useMemo(() => { const indexOfLastItem = currentPage * pageNo; @@ -78,14 +76,14 @@ const [pageNo,setpageNo] = useState(20); }, []); const handleFolderAdded = useCallback(async () => { - generateThumbnail(folderPath); + generateThumbnailAPI([folderPath]); }, []); useEffect(() => { - generateThumbnail(folderPath); + generateThumbnailAPI([folderPath]); }, [folderPath]); - if (isCreating || loading) { + if (isGeneratingThumbnails || isGeneratingTags) { return (
@@ -105,7 +103,7 @@ const [pageNo,setpageNo] = useState(20); setFilterTag={setFilterTag} mediaItems={mediaItems} onFolderAdded={handleFolderAdded} - isLoading={loading} + isLoading={isGeneratingTags} isVisibleSelectedImage={isVisibleSelectedImage} setIsVisibleSelectedImage={setIsVisibleSelectedImage} /> @@ -119,44 +117,47 @@ const [pageNo,setpageNo] = useState(20); openMediaViewer={openMediaViewer} type={type} /> -
- {/* Pagination Controls - Centered */} - +
+ {/* Pagination Controls - Centered */} + - {/* Dropdown Menu - Right-Aligned */} -
- - - - - + + +
+

+ Num of images per page : {pageNo} +

+ + + + setpageNo(Number(value))} + > + {noOfPages.map((itemsPerPage) => ( + + {itemsPerPage} + + ))} + + + +
)} @@ -174,4 +175,3 @@ const [pageNo,setpageNo] = useState(20);
); } - diff --git a/frontend/src/components/AITagging/FilterControls.tsx b/frontend/src/components/AITagging/FilterControls.tsx index 4aed312e..541fd1b5 100644 --- a/frontend/src/components/AITagging/FilterControls.tsx +++ b/frontend/src/components/AITagging/FilterControls.tsx @@ -9,7 +9,7 @@ import { import { Button } from '../ui/button'; import { MediaItem } from '@/types/Media'; -import FolderPicker from '../FolderPicker/FolderPicker'; +import AITaggingFolderPicker from '../FolderPicker/AITaggingFolderPicker'; import LoadingScreen from '../ui/LoadingScreen/LoadingScreen'; import DeleteSelectedImagePage from '../FolderPicker/DeleteSelectedImagePage'; import ErrorDialog from '../Album/Error'; @@ -96,7 +96,7 @@ export default function FilterControls({
Error: {errorMessage}
)}
- + - diff --git a/frontend/src/components/Album/Albumview.tsx b/frontend/src/components/Album/Albumview.tsx index 1a425b44..2b388629 100644 --- a/frontend/src/components/Album/Albumview.tsx +++ b/frontend/src/components/Album/Albumview.tsx @@ -80,9 +80,7 @@ const AlbumView: React.FC = ({ const convertedImagePaths = albumData.photos.map((path) => { return { url: convertFileSrc(path), - thumbnailUrl: convertFileSrc( - extractThumbnailPath(albumData.folder_path, path), - ), + thumbnailUrl: convertFileSrc(extractThumbnailPath(path)), }; }); diff --git a/frontend/src/components/Album/ImageSelection.tsx b/frontend/src/components/Album/ImageSelection.tsx index e3a563e7..949c9c07 100644 --- a/frontend/src/components/Album/ImageSelection.tsx +++ b/frontend/src/components/Album/ImageSelection.tsx @@ -45,9 +45,7 @@ const ImageSelectionPage: React.FC = ({ const imagesWithThumbnails = allImages.map((imagePath) => ({ imagePath, url: convertFileSrc(imagePath), - thumbnailUrl: convertFileSrc( - extractThumbnailPath(allImagesData.folder_path, imagePath), - ), + thumbnailUrl: convertFileSrc(extractThumbnailPath(imagePath)), })); useEffect(() => { if (errorMessage && errorMessage !== 'Something went wrong') { diff --git a/frontend/src/components/FolderPicker/AITaggingFolderPicker.tsx b/frontend/src/components/FolderPicker/AITaggingFolderPicker.tsx new file mode 100644 index 00000000..8503ce21 --- /dev/null +++ b/frontend/src/components/FolderPicker/AITaggingFolderPicker.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Button } from '../ui/button'; +import { open } from '@tauri-apps/plugin-dialog'; +import { FolderPlus } from 'lucide-react'; +interface FolderPickerProps { + setFolderPath: (path: string) => void; + className?: string; + handleDeleteCache?: () => void; +} + +const AITaggingFolderPicker: React.FC = ({ + setFolderPath, + className, + handleDeleteCache, +}) => { + const pickFolder = async () => { + try { + const selected = await open({ + directory: true, + multiple: false, + title: 'Select a folder', + }); + if (selected && typeof selected === 'string') { + setFolderPath(selected); + console.log('Selected folder:', selected); + if (handleDeleteCache) { + handleDeleteCache(); + } + } + } catch (error) { + console.error('Error picking folder:', error); + } + }; + return ( +
+ +
+ ); +}; + +export default AITaggingFolderPicker; diff --git a/frontend/src/components/FolderPicker/DeleteSelectedImagePage.tsx b/frontend/src/components/FolderPicker/DeleteSelectedImagePage.tsx index 54b7ed54..168e7d95 100644 --- a/frontend/src/components/FolderPicker/DeleteSelectedImagePage.tsx +++ b/frontend/src/components/FolderPicker/DeleteSelectedImagePage.tsx @@ -29,7 +29,7 @@ const DeleteSelectedImagePage: React.FC = ({ setIsVisibleSelectedImage, onError, uniqueTags, - mediaItems + mediaItems, }) => { const [selectedImages, setSelectedImages] = useState([]); @@ -38,16 +38,15 @@ const DeleteSelectedImagePage: React.FC = ({ queryKey: ['all-images'], }); - const { mutate: deleteMultipleImages, isPending: isAddingImages } = - usePictoMutation({ - mutationFn: delMultipleImages, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['all-images'] }); - }, - autoInvalidateTags: ['ai-tagging-images', 'ai'], - }); - + usePictoMutation({ + mutationFn: delMultipleImages, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['all-images'] }); + }, + autoInvalidateTags: ['ai-tagging-images', 'ai'], + }); + // Extract the array of image paths const allImages: string[] = response?.image_files || []; const toggleImageSelection = (imagePath: string) => { @@ -73,33 +72,28 @@ const DeleteSelectedImagePage: React.FC = ({ } }; - const [filterTag, setFilterTag] = useState(uniqueTags[0]); const handleFilterTag = (value: string) => { - setSelectedImages([]); - setFilterTag(value); - - if(value.length === 0) { + setSelectedImages([]); + setFilterTag(value); + + if (value.length === 0) { setSelectedImages(allImages); return; } const selectedImagesPaths: string[] = []; - + mediaItems.forEach((ele) => { if (ele.tags?.includes(value)) { selectedImagesPaths.push(ele.imagePath); } }); - - console.log("Selected Images Path = ", selectedImagesPaths); + + console.log('Selected Images Path = ', selectedImagesPaths); setSelectedImages(selectedImagesPaths); }; - - - - const getImageName = (path: string) => { return path.split('\\').pop() || path; @@ -126,7 +120,7 @@ const DeleteSelectedImagePage: React.FC = ({ >

- Select Tag : {filterTag || 'tags'} + Select Tag : {filterTag || 'tags'}

@@ -138,7 +132,7 @@ const DeleteSelectedImagePage: React.FC = ({ handleFilterTag(value)} + onValueChange={(value) => handleFilterTag(value)} > void; + setFolderPaths: (paths: string[]) => void; className?: string; - settingsPage?: boolean; - setIsLoading?: (loading: boolean) => void; - handleDeleteCache?: () => void; } const FolderPicker: React.FC = ({ - setFolderPath, + setFolderPaths, className, - settingsPage, - setIsLoading, - handleDeleteCache, }) => { - const { mutate: generateThumbnail } = usePictoMutation({ - mutationFn: generateThumbnails, - onSuccess: () => { - if (setIsLoading) { - setIsLoading(false); - } - }, - autoInvalidateTags: ['ai-tagging-images', 'ai'], - }); - const pickFolder = async () => { try { const selected = await open({ directory: true, - multiple: false, - title: 'Select a folder', + multiple: true, // Allow multiple folder selection + title: 'Select folders', }); - if (selected && typeof selected === 'string') { - setFolderPath(selected); - console.log('Selected folder:', selected); - if (settingsPage) { - setIsLoading && setIsLoading(true); - generateThumbnail(selected); - } - if (handleDeleteCache) { - handleDeleteCache(); - } + if (selected && Array.isArray(selected)) { + setFolderPaths(selected); } } catch (error) { - console.error('Error picking folder:', error); + console.error('Error picking folders:', error); } }; + return (
); diff --git a/frontend/src/components/Media/MediaGallery.tsx b/frontend/src/components/Media/MediaGallery.tsx index f1cc477c..d428ef1e 100644 --- a/frontend/src/components/Media/MediaGallery.tsx +++ b/frontend/src/components/Media/MediaGallery.tsx @@ -5,6 +5,9 @@ import SortingControls from './SortningControls'; import PaginationControls from '../ui/PaginationControls'; import { MediaGalleryProps } from '@/types/Media'; import { sortMedia } from '@/utils/Media'; +import { Button } from '@/components/ui/button'; +import { RefreshCw } from 'lucide-react'; +import { deleteCache } from '@/services/cacheService'; export default function MediaGallery({ mediaItems, @@ -43,16 +46,37 @@ export default function MediaGallery({ const closeMediaViewer = useCallback(() => { setShowMediaViewer(false); }, []); + const handleRefreshClick = async () => { + try { + const result = await deleteCache(); + if (result) { + console.log('Cache deleted'); + } + window.location.reload(); + } catch (error) { + console.error('Error deleting cache:', error); + } + }; return (

{title || currentYear}

- +
+ + +
= ({ {type === 'image' ? (
{ + if (e.target === e.currentTarget) { + onClose(); + } + }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} @@ -184,4 +190,4 @@ const MediaView: React.FC = ({ ); }; -export default MediaView; \ No newline at end of file +export default MediaView; diff --git a/frontend/src/components/Media/SortningControls.tsx b/frontend/src/components/Media/SortningControls.tsx index bbfbf2c1..54eee146 100644 --- a/frontend/src/components/Media/SortningControls.tsx +++ b/frontend/src/components/Media/SortningControls.tsx @@ -49,7 +49,7 @@ const SortingControls: React.FC = ({ variant="outline" className="flex items-center gap-2 border-gray-500" > - + {`Sort: ${sortingOptions.find((opt) => opt.value === sortBy)?.label || 'Select'}`} diff --git a/frontend/src/components/Navigation/Navbar/Navbar.tsx b/frontend/src/components/Navigation/Navbar/Navbar.tsx index 0d7540dc..6ae80124 100644 --- a/frontend/src/components/Navigation/Navbar/Navbar.tsx +++ b/frontend/src/components/Navigation/Navbar/Navbar.tsx @@ -33,7 +33,7 @@ export function Navbar({ title, onNameChange }: NavbarProps) { } } }, - [onNameChange] + [onNameChange], ); const handleNameClick = useCallback(() => { @@ -53,12 +53,12 @@ export function Navbar({ title, onNameChange }: NavbarProps) { } setIsEditing(false); }, - [onNameChange] + [onNameChange], ); return (
-
+
PictoPy Logo - + PictoPy
@@ -91,7 +91,7 @@ export function Navbar({ title, onNameChange }: NavbarProps) { ) : ( +
+ )) + ) : ( +
+ No folder paths selected +
+ )}
+ setErrorDialogContent(null)} + />
); }; diff --git a/frontend/src/pages/Setupscreen/Setup.tsx b/frontend/src/pages/Setupscreen/Setup.tsx index 5fdda2ff..c61f0c26 100644 --- a/frontend/src/pages/Setupscreen/Setup.tsx +++ b/frontend/src/pages/Setupscreen/Setup.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import { useInitialPageController } from '@/controllers/SetupController'; -import { SetupScreen } from '@/features/Setup/Setup'; +import { useInitialPageController } from '@/controllers/InitialPageController'; +import { SetupScreen } from '@/features/Setup/SetupScreen'; import { LoadingScreen } from '@/components/LoadingScreen/LoadingScreen'; export const InitialPage: React.FC = () => { - const { loading, handleFolderPathChange } = useInitialPageController(); + const { loading, handleFolderPathsChange } = useInitialPageController(); if (loading) { return ; } - return ; + return ; }; diff --git a/frontend/src/pages/VideosPage/Videos.tsx b/frontend/src/pages/VideosPage/Videos.tsx index bf4a96d8..04f1cda7 100644 --- a/frontend/src/pages/VideosPage/Videos.tsx +++ b/frontend/src/pages/VideosPage/Videos.tsx @@ -1,10 +1,11 @@ import { LoadingScreen } from '@/components/ui/LoadingScreen/LoadingScreen'; import MediaGallery from '@/components/Media/MediaGallery'; import { useVideos } from '@/hooks/UseVideos'; +import { useLocalStorage } from '@/hooks/LocalStorage'; const Videos: React.FC = () => { - const localPath = localStorage.getItem('folderPath') || ''; - const { videos, loading } = useVideos(localPath); + const [currentPaths] = useLocalStorage('folderPaths', []); + const { videos, loading } = useVideos(currentPaths); if (loading) { return ; diff --git a/frontend/src/types/Album.ts b/frontend/src/types/Album.ts index 0d97d56a..4ad8df0f 100644 --- a/frontend/src/types/Album.ts +++ b/frontend/src/types/Album.ts @@ -29,8 +29,7 @@ export interface AlbumData { } export interface CreateAlbumFormProps { isOpen: boolean; - onClose: () => void; - onSuccess: () => void; + closeForm: () => void; onError: (title: string, error: unknown) => void; } diff --git a/frontend/src/types/Media.ts b/frontend/src/types/Media.ts index 3219c37d..a9cbf54f 100644 --- a/frontend/src/types/Media.ts +++ b/frontend/src/types/Media.ts @@ -5,7 +5,7 @@ export interface MediaItem { date?: string; title?: string; tags?: string[]; - imagePath:string; + imagePath?: string; } export interface MediaCardProps { item: MediaItem; From de382739273a7e9c7b29abdc6d576d8f49ab99b1 Mon Sep 17 00:00:00 2001 From: Rahul Harpal Date: Sat, 4 Jan 2025 23:03:55 +0530 Subject: [PATCH 2/8] Fix image path error for unix-like paths --- frontend/src/hooks/useImages.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/hooks/useImages.ts b/frontend/src/hooks/useImages.ts index b099be48..af077497 100644 --- a/frontend/src/hooks/useImages.ts +++ b/frontend/src/hooks/useImages.ts @@ -74,7 +74,8 @@ export const useImages = (folderPaths: string[]) => { const mappedImages = await Promise.all( imagePaths.map(async (imagePath: string) => { const original = imagePath; - const fileName = imagePath.split('\\').pop(); + const cleanedImagePath = imagePath.replace(/\\/g, '/'); // Replaces all '\' with '/' + const fileName = cleanedImagePath.split('/').pop(); const url = convertFileSrc(imagePath); const thumbnailUrl = convertFileSrc( extractThumbnailPath(imagePath), From e132d8374f45548dc1558f38e368aec2615bc936 Mon Sep 17 00:00:00 2001 From: Rahul Harpal Date: Sat, 4 Jan 2025 23:20:37 +0530 Subject: [PATCH 3/8] thumbnail deletion error fixed --- backend/app/routes/images.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/app/routes/images.py b/backend/app/routes/images.py index 16b5194d..68fba698 100644 --- a/backend/app/routes/images.py +++ b/backend/app/routes/images.py @@ -251,21 +251,22 @@ def delete_multiple_images(payload: dict): }, ) path = os.path.normpath(path) - parts = path.split(os.sep) - parts.insert(parts.index("images") + 1, "PictoPy.thumbnails") - thumb_nail_image_path = os.sep.join(parts) + folder_path, filename = os.path.split(path) + thumbnail_folder = os.path.join(folder_path, "PictoPy.thumbnails") + thumb_nail_image_path = os.path.join(thumbnail_folder, filename) + # Check and remove the original file if os.path.exists(path): try: os.remove(path) except PermissionError: - print(f"Permission denied for file '{thumb_nail_image_path}'.") + print(f"Permission denied for file '{path}'.") except Exception as e: print(f"An error occurred: {e}") - else: print(f"File '{path}' does not exist.") + # Check and remove the thumbnail file if os.path.exists(thumb_nail_image_path): try: os.remove(thumb_nail_image_path) From e80ae2b3e5f0350f5f9d7f3438cf1228de44c9bf Mon Sep 17 00:00:00 2001 From: Rahul Harpal Date: Sat, 4 Jan 2025 23:55:00 +0530 Subject: [PATCH 4/8] Implement cache deletion on dashboard load --- frontend/src/pages/Dashboard/Dashboard.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/frontend/src/pages/Dashboard/Dashboard.tsx b/frontend/src/pages/Dashboard/Dashboard.tsx index ba9bd4dd..ba9ac1de 100644 --- a/frontend/src/pages/Dashboard/Dashboard.tsx +++ b/frontend/src/pages/Dashboard/Dashboard.tsx @@ -2,10 +2,25 @@ import MediaGallery from '@/components/Media/MediaGallery'; import LoadingScreen from '@/components/ui/LoadingScreen/LoadingScreen'; import { useImages } from '@/hooks/useImages'; import { useLocalStorage } from '@/hooks/LocalStorage'; +import { useEffect } from 'react'; +import { deleteCache } from '@/services/cacheService'; function Dashboard() { const [currentPaths] = useLocalStorage('folderPaths', []); const { images, isCreating: loading } = useImages(currentPaths); + useEffect(() => { + const func = async () => { + try { + const result = await deleteCache(); + if (result) { + console.log('Cache deleted'); + } + } catch (error) { + console.error('Error deleting cache:', error); + } + }; + func(); + }, [currentPaths]); if (loading) { return ; } From ed42880c455c19ec6b9dc207e0426a6cc5480948 Mon Sep 17 00:00:00 2001 From: Rahul Harpal Date: Mon, 6 Jan 2025 15:37:21 +0530 Subject: [PATCH 5/8] Fix null checks for image paths in DeleteSelectedImagePage component --- .../FolderPicker/DeleteSelectedImagePage.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/FolderPicker/DeleteSelectedImagePage.tsx b/frontend/src/components/FolderPicker/DeleteSelectedImagePage.tsx index 34308a52..60d2eb66 100644 --- a/frontend/src/components/FolderPicker/DeleteSelectedImagePage.tsx +++ b/frontend/src/components/FolderPicker/DeleteSelectedImagePage.tsx @@ -87,7 +87,7 @@ const DeleteSelectedImagePage: React.FC = ({ mediaItems.forEach((ele) => { if (ele.tags?.includes(value)) { - selectedImagesPaths.push(ele.imagePath); + ele.imagePath && selectedImagesPaths.push(ele.imagePath); } }); @@ -160,20 +160,20 @@ const DeleteSelectedImagePage: React.FC = ({ return (
toggleImageSelection(imagePath)} + onClick={() => imagePath && toggleImageSelection(imagePath)} /> {`Image
- {getImageName(imagePath)} + {imagePath && getImageName(imagePath)}
); From 1e8b70393a30c02d010c0fd697ed2e9c0093d3cf Mon Sep 17 00:00:00 2001 From: rahulharpal1603 Date: Sun, 26 Jan 2025 16:38:08 +0530 Subject: [PATCH 6/8] Using thumbnail urls instead of actual url while in MediaView component. --- frontend/src/components/Media/MediaGallery.tsx | 6 +++++- frontend/src/components/Media/MediaView.tsx | 2 +- frontend/src/types/Media.ts | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Media/MediaGallery.tsx b/frontend/src/components/Media/MediaGallery.tsx index 2783ac24..2bd8811e 100644 --- a/frontend/src/components/Media/MediaGallery.tsx +++ b/frontend/src/components/Media/MediaGallery.tsx @@ -142,7 +142,11 @@ export default function MediaGallery({ initialIndex={selectedMediaIndex} onClose={closeMediaViewer} allMedia={sortedMedia.map((item) => { - return { url: item.url, path: item?.imagePath }; + return { + url: item.url, + path: item?.imagePath, + thumbnailUrl: item.thumbnailUrl, + }; })} currentPage={currentPage} itemsPerPage={itemsPerPage} diff --git a/frontend/src/components/Media/MediaView.tsx b/frontend/src/components/Media/MediaView.tsx index 7afb7b61..09a2db1a 100644 --- a/frontend/src/components/Media/MediaView.tsx +++ b/frontend/src/components/Media/MediaView.tsx @@ -495,7 +495,7 @@ const MediaView: React.FC = ({ )} {type === 'image' ? ( {`thumbnail-${index}`} diff --git a/frontend/src/types/Media.ts b/frontend/src/types/Media.ts index 02d87e79..dea3c8bd 100644 --- a/frontend/src/types/Media.ts +++ b/frontend/src/types/Media.ts @@ -27,7 +27,7 @@ export interface MediaGridProps { export interface MediaViewProps { initialIndex: number; onClose: () => void; - allMedia: { url: string; path?: string }[]; + allMedia: { url: string; path?: string; thumbnailUrl?: string }[]; currentPage: number; itemsPerPage: number; type: 'image' | 'video'; From d6c45d5e74d6432071eb0400af8e4bc0a0a1e9f3 Mon Sep 17 00:00:00 2001 From: rahulharpal1603 Date: Tue, 4 Feb 2025 02:50:52 +0530 Subject: [PATCH 7/8] reenable multiple folder paths in memories. --- frontend/src/components/Memories/Memories.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/components/Memories/Memories.tsx b/frontend/src/components/Memories/Memories.tsx index b50b2e8f..f7dbb15f 100644 --- a/frontend/src/components/Memories/Memories.tsx +++ b/frontend/src/components/Memories/Memories.tsx @@ -22,8 +22,7 @@ const Memories: React.FC = () => { const [storyIndex, setStoryIndex] = useState(0); const itemsPerPage = 12; const [currentPath] = useLocalStorage('folderPath', ''); - // const [currentPaths] = useLocalStorage('folderPaths', []); Temporarily commented out, will be uncommented after open PR related to multiple folder support is merged. - const currentPaths: string[] = []; // Temporarily added to avoid TypeScript error, will be removed after open PR related to multiple folder support is merged. + const [currentPaths] = useLocalStorage('folderPaths', []); const storyDuration = 3000; // 3 seconds per story useEffect(() => { From faa1d682c2b2c2e2c984038264c136ea7ce19d1b Mon Sep 17 00:00:00 2001 From: rahulharpal1603 Date: Fri, 7 Feb 2025 18:42:16 +0530 Subject: [PATCH 8/8] Update GitHub Actions workflow to trigger on pull requests and specify paths for backend and frontend --- .github/workflows/merge.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/merge.yml b/.github/workflows/merge.yml index f2bcee4c..fdcf8bdb 100644 --- a/.github/workflows/merge.yml +++ b/.github/workflows/merge.yml @@ -3,8 +3,12 @@ name: Merge Check on: pull_request_review: - # Trigger this workflow when a pull request review is submitted + # Trigger this workflow when a pull request review is submitted types: [submitted] + pull_request: + paths: + - "backend/**" + - "frontend/**" jobs: build-tauri: @@ -27,22 +31,22 @@ jobs: uses: actions/setup-node@v4 with: node-version: lts/* - cache: 'npm' - + cache: "npm" + - name: Install Rust uses: dtolnay/rust-toolchain@stable - name: Rust cache uses: swatinem/rust-cache@v2 with: - # Cache Rust build artifacts for faster build times - workspaces: './src-tauri -> target' - + # Cache Rust build artifacts for faster build times + workspaces: "./src-tauri -> target" + - name: Install frontend dependencies run: | cd frontend npm install - + - name: Build Tauri uses: tauri-apps/tauri-action@v0 with: @@ -51,4 +55,4 @@ jobs: env: # Use secrets for signing the Tauri app TAURI_SIGNING_PRIVATE_KEY: dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5NlF2SjE3cWNXOVlQQ0JBTlNITEpOUVoyQ3ZuNTdOSkwyNE1NN2RmVWQ1a0FBQkFBQUFBQUFBQUFBQUlBQUFBQU9XOGpTSFNRd0Q4SjNSbm5Oc1E0OThIUGx6SS9lWXI3ZjJxN3BESEh1QTRiQXlkR2E5aG1oK1g0Tk5kcmFzc0IvZFZScEpubnptRkxlbDlUR2R1d1Y5OGRSYUVmUGoxNTFBcHpQZ1dSS2lHWklZVHNkV1Byd1VQSnZCdTZFWlVGOUFNVENBRlgweUU9Cg== - TAURI_SIGNING_PRIVATE_KEY_PASSWORD : pass + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: pass