diff --git a/backend/app/database/images.py b/backend/app/database/images.py index ec9541a56..4a20ff470 100644 --- a/backend/app/database/images.py +++ b/backend/app/database/images.py @@ -419,3 +419,22 @@ def db_toggle_image_favourite_status(image_id: str) -> bool: return False finally: conn.close() + + +def db_get_total_image_count() -> int: + """ + Get the total number of images in the database. + Returns: + Total count of images + """ + conn = _connect() + cursor = conn.cursor() + try: + cursor.execute("SELECT COUNT(*) FROM images") + result = cursor.fetchone() + return result[0] if result else 0 + except Exception as e: + logger.error(f"Error getting total image count: {e}") + return 0 + finally: + conn.close() diff --git a/backend/app/routes/folders.py b/backend/app/routes/folders.py index a66cca27c..d7a550042 100644 --- a/backend/app/routes/folders.py +++ b/backend/app/routes/folders.py @@ -11,6 +11,7 @@ db_get_folder_ids_by_path_prefix, db_get_all_folder_details, ) +from app.database.images import db_get_total_image_count from app.logging.setup_logging import get_logger from app.schemas.folders import ( AddFolderRequest, @@ -460,9 +461,15 @@ def get_all_folders(): taggingCompleted=tagging_completed, ) ) + + total_images = db_get_total_image_count() return GetAllFoldersResponse( - data=GetAllFoldersData(folders=folders, total_count=len(folders)), + data=GetAllFoldersData( + folders=folders, + total_count=len(folders), + total_images=total_images + ), success=True, message=f"Successfully retrieved {len(folders)} folder(s)", ) diff --git a/backend/app/schemas/folders.py b/backend/app/schemas/folders.py index 63045241b..8bdb240fc 100644 --- a/backend/app/schemas/folders.py +++ b/backend/app/schemas/folders.py @@ -35,6 +35,7 @@ class FolderDetails(BaseModel): class GetAllFoldersData(BaseModel): folders: List[FolderDetails] total_count: int + total_images: int class AddFolderData(BaseModel): diff --git a/docs/backend/backend_python/openapi.json b/docs/backend/backend_python/openapi.json index fbf40091b..678286e3a 100644 --- a/docs/backend/backend_python/openapi.json +++ b/docs/backend/backend_python/openapi.json @@ -1785,12 +1785,17 @@ "total_count": { "type": "integer", "title": "Total Count" + }, + "total_images": { + "type": "integer", + "title": "Total Images" } }, "type": "object", "required": [ "folders", - "total_count" + "total_count", + "total_images" ], "title": "GetAllFoldersData" }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ab218ecaf..f1630ae03 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -73,6 +73,7 @@ "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.20", "babel-jest": "^29.7.0", + "baseline-browser-mapping": "^2.9.11", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-config-react-app": "^7.0.1", @@ -6588,9 +6589,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz", - "integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==", + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/frontend/package.json b/frontend/package.json index 0a53f1b8d..147467cca 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -88,6 +88,7 @@ "@vitejs/plugin-react": "^4.2.1", "autoprefixer": "^10.4.20", "babel-jest": "^29.7.0", + "baseline-browser-mapping": "^2.9.11", "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-config-react-app": "^7.0.1", diff --git a/frontend/src/features/folderSlice.ts b/frontend/src/features/folderSlice.ts index 2efbe42f6..65f0b6b0b 100644 --- a/frontend/src/features/folderSlice.ts +++ b/frontend/src/features/folderSlice.ts @@ -6,11 +6,13 @@ interface FolderState { folders: FolderDetails[]; taggingStatus: Record; lastUpdatedAt?: number; + totalImages: number; } const initialState: FolderState = { folders: [], taggingStatus: {}, + totalImages: 0, }; const folderSlice = createSlice({ @@ -22,6 +24,11 @@ const folderSlice = createSlice({ state.folders = action.payload; }, + // Set total images + setTotalImages(state, action: PayloadAction) { + state.totalImages = action.payload; + }, + // Add a single folder addFolder(state, action: PayloadAction) { const newFolder = action.payload; @@ -69,6 +76,7 @@ const folderSlice = createSlice({ // Clear all folder data clearFolders(state) { state.folders = []; + state.totalImages = 0; }, // Set tagging status for folders @@ -91,6 +99,7 @@ const folderSlice = createSlice({ export const { setFolders, + setTotalImages, addFolder, updateFolder, removeFolders, diff --git a/frontend/src/hooks/useFolderOperations.tsx b/frontend/src/hooks/useFolderOperations.tsx index ff747ff0b..45f5047c9 100644 --- a/frontend/src/hooks/useFolderOperations.tsx +++ b/frontend/src/hooks/useFolderOperations.tsx @@ -8,7 +8,7 @@ import { deleteFolders, } from '@/api/api-functions'; import { selectAllFolders } from '@/features/folderSelectors'; -import { setFolders, setTaggingStatus } from '@/features/folderSlice'; +import { setFolders, setTaggingStatus, setTotalImages } from '@/features/folderSlice'; import { FolderDetails } from '@/types/Folder'; import { useMutationFeedback } from './useMutationFeedback'; import { getFoldersTaggingStatus } from '@/api/api-functions/folders'; @@ -61,6 +61,12 @@ export const useFolderOperations = () => { if (foldersQuery.data?.data?.folders) { const folders = foldersQuery.data.data.folders as FolderDetails[]; dispatch(setFolders(folders)); + dispatch( + setTotalImages( + (foldersQuery.data.data as unknown as { total_images: number }) + .total_images || 0, + ), + ); } }, [foldersQuery.data, dispatch]); diff --git a/frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx b/frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx index db4b029fa..04551b5eb 100644 --- a/frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx +++ b/frontend/src/pages/SettingsPage/components/FolderManagementCard.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { Folder, Trash2, Check } from 'lucide-react'; +import { Folder, Trash2, Check, ChevronDown, ChevronUp } from 'lucide-react'; +import { useState } from 'react'; import { Switch } from '@/components/ui/switch'; import { Button } from '@/components/ui/button'; @@ -25,112 +26,137 @@ const FolderManagementCard: React.FC = () => { deleteFolderPending, } = useFolderOperations(); + const [isExpanded, setIsExpanded] = useState(true); + const taggingStatus = useSelector( (state: RootState) => state.folders.taggingStatus, ); + const totalImages = useSelector((state: RootState) => state.folders.totalImages); + return ( setIsExpanded(!isExpanded)} + > + {isExpanded ? ( + + ) : ( + + )} + + } > - {folders.length > 0 ? ( -
- {folders.map((folder: FolderDetails, index: number) => ( -
-
-
-
- - - {folder.folder_path} - -
-
- -
-
- - AI Tagging - - toggleAITagging(folder)} - disabled={ - enableAITaggingPending || disableAITaggingPending - } - /> -
+ {isExpanded && ( +
+ {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' - } - /> + + {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' + } + /> +
+ )}
- )} + ))}
- ))} -
- ) : ( -
- -

- No folders configured -

-

- Add your first photo library folder to get started -

+ ) : ( +
+ +

+ No folders configured +

+

+ Add your first photo library folder to get started +

+
+ )} + +
+ +
)} - -
- -
); }; diff --git a/frontend/src/pages/SettingsPage/components/SettingsCard.tsx b/frontend/src/pages/SettingsPage/components/SettingsCard.tsx index 183be138a..0d7b8d1b1 100644 --- a/frontend/src/pages/SettingsPage/components/SettingsCard.tsx +++ b/frontend/src/pages/SettingsPage/components/SettingsCard.tsx @@ -14,6 +14,10 @@ interface SettingsCardProps { * Card description */ description?: string; + /** + * Optional action element to display in the header + */ + action?: React.ReactNode; /** * Card content */ @@ -27,6 +31,7 @@ const SettingsCard: React.FC = ({ icon: Icon, title, description, + action, children, }) => { return ( @@ -39,6 +44,7 @@ const SettingsCard: React.FC = ({

{description}

)}
+ {action &&
{action}
}
{children}
diff --git a/frontend/src/types/Folder.ts b/frontend/src/types/Folder.ts index 34a9f9c58..f6498c450 100644 --- a/frontend/src/types/Folder.ts +++ b/frontend/src/types/Folder.ts @@ -10,4 +10,5 @@ export interface FolderDetails { export interface GetAllFoldersData { folders: FolderDetails[]; total_count: number; + total_images: number; }