diff --git a/frontend/package.json b/frontend/package.json index 0a53f1b8d..c83bc4d1f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,6 +25,7 @@ "dependencies": { "@radix-ui/react-aspect-ratio": "^1.1.7", "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", @@ -104,4 +105,4 @@ "vite": "^6.3.5", "vite-plugin-eslint": "^1.8.1" } -} +} \ No newline at end of file diff --git a/frontend/src/hooks/useFolderOperations.tsx b/frontend/src/hooks/useFolderOperations.tsx index 0c0fcc559..0a4b4db0d 100644 --- a/frontend/src/hooks/useFolderOperations.tsx +++ b/frontend/src/hooks/useFolderOperations.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { usePictoMutation, usePictoQuery } from '@/hooks/useQueryExtension'; import { @@ -15,7 +15,7 @@ import { getFoldersTaggingStatus } from '@/api/api-functions/folders'; /** * Custom hook for folder operations - * Manages folder queries, AI tagging mutations, and folder deletion + * Manages folder queries, AI tagging mutations, folder deletion, and bulk operations */ export const useFolderOperations = () => { const dispatch = useDispatch(); @@ -160,19 +160,83 @@ export const useFolderOperations = () => { deleteFolderMutation.mutate(folderId); }; + // Bulk enable AI tagging mutation + const bulkEnableAITaggingMutation = usePictoMutation({ + mutationFn: async (folder_ids: string[]) => + enableAITagging({ folder_ids }), + autoInvalidateTags: ['folders'], + }); + + // Apply feedback to bulk enable mutation + useMutationFeedback(bulkEnableAITaggingMutation, { + showLoading: true, + loadingMessage: 'Enabling AI tagging for selected folders', + successTitle: 'AI Tagging Enabled', + successMessage: 'AI tagging has been enabled for the selected folders.', + errorTitle: 'AI Tagging Error', + errorMessage: 'Failed to enable AI tagging for some folders.', + }); + + // Bulk disable AI tagging mutation + const bulkDisableAITaggingMutation = usePictoMutation({ + mutationFn: async (folder_ids: string[]) => + disableAITagging({ folder_ids }), + autoInvalidateTags: ['folders'], + }); + + // Apply feedback to bulk disable mutation + useMutationFeedback(bulkDisableAITaggingMutation, { + showLoading: true, + loadingMessage: 'Disabling AI tagging for selected folders', + successTitle: 'AI Tagging Disabled', + successMessage: 'AI tagging has been disabled for the selected folders.', + errorTitle: 'AI Tagging Error', + errorMessage: 'Failed to disable AI tagging for some folders.', + }); + + /** + * Enable AI tagging for multiple folders + */ + const bulkEnableAITagging = useCallback( + (folderIds: string[]) => { + if (folderIds.length > 0) { + bulkEnableAITaggingMutation.mutate(folderIds); + } + }, + [bulkEnableAITaggingMutation], + ); + + /** + * Disable AI tagging for multiple folders + */ + const bulkDisableAITagging = useCallback( + (folderIds: string[]) => { + if (folderIds.length > 0) { + bulkDisableAITaggingMutation.mutate(folderIds); + } + }, + [bulkDisableAITaggingMutation], + ); + return { // Data folders, isLoading: foldersQuery.isLoading, - // Operations + // Single folder operations toggleAITagging, deleteFolder, + // Bulk operations + bulkEnableAITagging, + bulkDisableAITagging, + // Mutation states (for use in UI, e.g., disabling buttons) enableAITaggingPending: enableAITaggingMutation.isPending, disableAITaggingPending: disableAITaggingMutation.isPending, deleteFolderPending: deleteFolderMutation.isPending, + bulkEnablePending: bulkEnableAITaggingMutation.isPending, + bulkDisablePending: bulkDisableAITaggingMutation.isPending, }; }; diff --git a/frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx b/frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx index db4b029fa..332ac12eb 100644 --- a/frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx +++ b/frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx @@ -1,34 +1,146 @@ -import React from 'react'; -import { Folder, Trash2, Check } from 'lucide-react'; - -import { Switch } from '@/components/ui/switch'; -import { Button } from '@/components/ui/button'; -import { Progress } from '@/components/ui/progress'; +import React, { useState, useMemo, useEffect, useCallback } from 'react'; +import { Folder } from 'lucide-react'; import { useSelector } from 'react-redux'; import { RootState } from '@/app/store'; import FolderPicker from '@/components/FolderPicker/FolderPicker'; - import { useFolderOperations } from '@/hooks/useFolderOperations'; -import { FolderDetails } from '@/types/Folder'; +import { useBulkFolderSelection } from '@/hooks/useBulkFolderSelection'; import SettingsCard from './SettingsCard'; +import { + FolderProgressSummary, + FolderBulkActions, + FolderSection, +} from '@/components/FolderManagement'; +import { + mergeFoldersWithStatus, + groupFoldersByStatus, + calculateFolderStats, + loadFolderPreferences, + saveFolderPreferences, + FolderPreferences, + FolderWithStatus, +} from '@/utils/folderUtils'; /** * Component for managing folder operations in settings + * Supports bulk AI tagging, progress tracking, and smart sorting */ const FolderManagementCard: React.FC = () => { const { folders, toggleAITagging, deleteFolder, + bulkEnableAITagging, + bulkDisableAITagging, enableAITaggingPending, disableAITaggingPending, deleteFolderPending, + bulkEnablePending, + bulkDisablePending, } = useFolderOperations(); const taggingStatus = useSelector( (state: RootState) => state.folders.taggingStatus, ); + // Selection state + const { + selectedIds, + toggleSelection, + selectAll, + deselectAll, + selectedCount, + isAllSelected, + getSelectedFolders, + } = useBulkFolderSelection(); + + // Collapsed sections state (persisted) + const [preferences, setPreferences] = useState( + loadFolderPreferences, + ); + + // Save preferences when they change + useEffect(() => { + saveFolderPreferences(preferences); + }, [preferences]); + + // Merge folders with their tagging status + const foldersWithStatus: FolderWithStatus[] = useMemo( + () => mergeFoldersWithStatus(folders, taggingStatus), + [folders, taggingStatus], + ); + + // Group folders by status + const groupedFolders = useMemo( + () => groupFoldersByStatus(foldersWithStatus), + [foldersWithStatus], + ); + + // Calculate statistics + const stats = useMemo( + () => calculateFolderStats(groupedFolders), + [groupedFolders], + ); + + // Toggle section collapse + const toggleSectionCollapse = useCallback( + (section: 'completed' | 'inProgress' | 'pending') => { + setPreferences((prev) => ({ + ...prev, + collapsedSections: { + ...prev.collapsedSections, + [section]: !prev.collapsedSections[section], + }, + })); + }, + [], + ); + + // Bulk action handlers + const handleSelectAll = useCallback(() => { + selectAll(foldersWithStatus); + }, [selectAll, foldersWithStatus]); + + const handleEnableAll = useCallback(() => { + const pendingFolderIds = groupedFolders.pending.map((f) => f.folder_id); + bulkEnableAITagging(pendingFolderIds); + }, [groupedFolders.pending, bulkEnableAITagging]); + + const handleEnableSelected = useCallback(() => { + const selectedFolders = getSelectedFolders(foldersWithStatus); + const pendingSelectedIds = selectedFolders + .filter((f) => !f.AI_Tagging) + .map((f) => f.folder_id); + if (pendingSelectedIds.length > 0) { + bulkEnableAITagging(pendingSelectedIds); + } else { + // If all selected already have AI tagging, enable anyway (re-trigger) + bulkEnableAITagging(selectedFolders.map((f) => f.folder_id)); + } + }, [getSelectedFolders, foldersWithStatus, bulkEnableAITagging]); + + const handleDisableAll = useCallback(() => { + const enabledFolderIds = [ + ...groupedFolders.completed, + ...groupedFolders.inProgress, + ].map((f) => f.folder_id); + bulkDisableAITagging(enabledFolderIds); + }, [groupedFolders, bulkDisableAITagging]); + + const handleDisableSelected = useCallback(() => { + const selectedFolders = getSelectedFolders(foldersWithStatus); + const enabledSelectedIds = selectedFolders + .filter((f) => f.AI_Tagging) + .map((f) => f.folder_id); + bulkDisableAITagging(enabledSelectedIds); + }, [getSelectedFolders, foldersWithStatus, bulkDisableAITagging]); + + const isTaggingPending = + enableAITaggingPending || + disableAITaggingPending || + bulkEnablePending || + bulkDisablePending; + return ( { description="Configure your photo library folders and AI settings" > {folders.length > 0 ? ( -
- {folders.map((folder: FolderDetails, index: number) => ( -
-
-
-
- - - {folder.folder_path} - -
-
- -
-
- - AI Tagging - - toggleAITagging(folder)} - disabled={ - enableAITaggingPending || disableAITaggingPending - } - /> -
- - -
-
- - {folder.AI_Tagging && ( -
-
- AI Tagging Progress - = 100 - ? 'flex items-center gap-1 text-green-500' - : 'text-muted-foreground' - } - > - {(taggingStatus[folder.folder_id]?.tagging_percentage ?? - 0) >= 100 && } - {Math.round( - taggingStatus[folder.folder_id]?.tagging_percentage ?? - 0, - )} - % - -
- = 100 - ? 'bg-green-500' - : 'bg-blue-500' - } - /> -
- )} -
- ))} -
+ <> + {/* Progress Summary */} + + + {/* Bulk Actions */} + + + {/* Folder Sections */} +
+ toggleSectionCollapse('inProgress')} + selectedIds={selectedIds} + onToggleSelection={toggleSelection} + onToggleAITagging={toggleAITagging} + onDeleteFolder={deleteFolder} + isTaggingPending={isTaggingPending} + isDeletePending={deleteFolderPending} + /> + + toggleSectionCollapse('pending')} + selectedIds={selectedIds} + onToggleSelection={toggleSelection} + onToggleAITagging={toggleAITagging} + onDeleteFolder={deleteFolder} + isTaggingPending={isTaggingPending} + isDeletePending={deleteFolderPending} + /> + + toggleSectionCollapse('completed')} + selectedIds={selectedIds} + onToggleSelection={toggleSelection} + onToggleAITagging={toggleAITagging} + onDeleteFolder={deleteFolder} + isTaggingPending={isTaggingPending} + isDeletePending={deleteFolderPending} + /> +
+ ) : (
-

- No folders configured -

-

- Add your first photo library folder to get started +

+ No folders added yet

+
)} -
- -
+ {folders.length > 0 && ( +
+ +
+ )}
); }; diff --git a/landing-page/src/Pages/Landing page/Home1.tsx b/landing-page/src/Pages/Landing page/Home1.tsx index 3483b722c..e79de6748 100644 --- a/landing-page/src/Pages/Landing page/Home1.tsx +++ b/landing-page/src/Pages/Landing page/Home1.tsx @@ -5,7 +5,7 @@ const ShuffleHero = () => { return (
- { > INTELLIGENT GALLERY MANAGEMENT - - { > PictoPy - - { > Advanced desktop gallery application powered by Tauri, React, and Rust with a Python backend for intelligent image analysis and seamless management. - +
- { > Download - + {/* Update this button to navigate to the GitHub link */} -