diff --git a/backend/app/database/folders_bulk.py b/backend/app/database/folders_bulk.py new file mode 100644 index 000000000..704351b00 --- /dev/null +++ b/backend/app/database/folders_bulk.py @@ -0,0 +1,122 @@ +# Database helpers for bulk operations +import sqlite3 +from typing import List, Dict, Any +from .connection import get_db_connection + +def db_count_images_in_folder(folder_id: str) -> int: + """ + Count total images in a folder. + """ + conn = get_db_connection() + cursor = conn.cursor() + + try: + cursor.execute( + "SELECT COUNT(*) FROM images WHERE folder_id = ?", + (folder_id,) + ) + count = cursor.fetchone()[0] + return count + finally: + conn.close() + +def db_count_tagged_images_in_folder(folder_id: str) -> int: + """ + Count images that have been AI tagged in a folder. + """ + conn = get_db_connection() + cursor = conn.cursor() + + try: + cursor.execute( + """ + SELECT COUNT(DISTINCT i.id) + FROM images i + JOIN tags t ON i.id = t.image_id + WHERE i.folder_id = ? + """, + (folder_id,) + ) + count = cursor.fetchone()[0] + return count + finally: + conn.close() + +def db_get_folders_summary() -> Dict[str, Dict[str, Any]]: + """ + Get summary statistics for all folders. + Optimized single-query approach. + """ + conn = get_db_connection() + cursor = conn.cursor() + + try: + query = """ + SELECT + f.id as folder_id, + f.name as folder_name, + f.ai_tagging_enabled, + COUNT(DISTINCT i.id) as total_images, + COUNT(DISTINCT CASE WHEN t.id IS NOT NULL THEN i.id END) as tagged_images + FROM folders f + LEFT JOIN images i ON f.id = i.folder_id + LEFT JOIN tags t ON i.id = t.image_id + GROUP BY f.id, f.name, f.ai_tagging_enabled + """ + + cursor.execute(query) + rows = cursor.fetchall() + + summary = {} + for row in rows: + folder_id = row['folder_id'] + total = row['total_images'] + tagged = row['tagged_images'] + + if total == 0: + status = 'empty' + progress = 0.0 + elif tagged == total: + status = 'completed' + progress = 100.0 + elif tagged > 0: + status = 'in_progress' + progress = (tagged / total) * 100 + else: + status = 'pending' + progress = 0.0 + + summary[folder_id] = { + 'folder_name': row['folder_name'], + 'ai_tagging_enabled': bool(row['ai_tagging_enabled']), + 'total_images': total, + 'tagged_images': tagged, + 'status': status, + 'progress_percentage': round(progress, 2) + } + + return summary + finally: + conn.close() + +def db_bulk_enable_tagging(folder_ids: List[str]) -> int: + """ + Enable AI tagging for multiple folders at once. + Returns number of folders updated. + """ + conn = get_db_connection() + cursor = conn.cursor() + + try: + placeholders = ','.join('?' * len(folder_ids)) + query = f""" + UPDATE folders + SET ai_tagging_enabled = 1 + WHERE id IN ({placeholders}) + """ + + cursor.execute(query, folder_ids) + conn.commit() + return cursor.rowcount + finally: + conn.close() diff --git a/backend/app/routes/folders.py b/backend/app/routes/folders.py index a66cca27c..bd6175e36 100644 --- a/backend/app/routes/folders.py +++ b/backend/app/routes/folders.py @@ -1,478 +1,127 @@ -from fastapi import APIRouter, HTTPException, status, Depends, Request -from typing import List, Tuple -from app.database.folders import ( - db_update_parent_ids_for_subtree, - db_folder_exists, - db_find_parent_folder_id, - db_enable_ai_tagging_batch, - db_disable_ai_tagging_batch, - db_delete_folders_batch, - db_get_direct_child_folders, - db_get_folder_ids_by_path_prefix, - db_get_all_folder_details, -) -from app.logging.setup_logging import get_logger -from app.schemas.folders import ( - AddFolderRequest, - AddFolderResponse, - AddFolderData, - ErrorResponse, - UpdateAITaggingRequest, - UpdateAITaggingResponse, - UpdateAITaggingData, - DeleteFoldersRequest, - DeleteFoldersResponse, - DeleteFoldersData, - SyncFolderRequest, - SyncFolderResponse, - SyncFolderData, - GetAllFoldersResponse, - GetAllFoldersData, - FolderDetails, -) -import os -from app.utils.folders import ( - folder_util_add_folder_tree, - folder_util_add_multiple_folder_trees, - folder_util_delete_obsolete_folders, - folder_util_get_filesystem_direct_child_folders, -) -from concurrent.futures import ProcessPoolExecutor -from app.utils.images import ( - image_util_process_folder_images, - image_util_process_untagged_images, -) -from app.utils.face_clusters import cluster_util_face_clusters_sync -from app.utils.API import API_util_restart_sync_microservice_watcher - -# Initialize logger -logger = get_logger(__name__) +# Add this to existing folders.py +from fastapi import APIRouter, HTTPException, BackgroundTasks +from pydantic import BaseModel +from typing import List +import asyncio router = APIRouter() +class BulkTagRequest(BaseModel): + folder_ids: List[str] -def post_folder_add_sequence(folder_path: str, folder_id: int): - """ - Post-addition sequence for a folder. - This function is called after a folder is successfully added. - It processes images in the folder and updates the database. - """ - try: - # Get all folder IDs and paths that match the root path prefix - folder_data = [] - folder_ids_and_paths = db_get_folder_ids_by_path_prefix(folder_path) - - # Set all folders to non-recursive (False) - for folder_id_from_db, folder_path_from_db in folder_ids_and_paths: - folder_data.append((folder_path_from_db, folder_id_from_db, False)) - - logger.info(f"Add folder: {folder_data}") - # Process images in all folders - image_util_process_folder_images(folder_data) - - # Restart sync microservice watcher after processing images - API_util_restart_sync_microservice_watcher() - - except Exception as e: - logger.error( - f"Error in post processing after folder {folder_path} was added: {e}" - ) - return False - return True - - -def post_AI_tagging_enabled_sequence(): - """ - Post-enabling AI tagging sequence. - This function is called after AI tagging is enabled for a folder. - It processes untagged images in the database. - """ - try: - image_util_process_untagged_images() - cluster_util_face_clusters_sync() - except Exception as e: - logger.error(f"Error in post processing after AI tagging was enabled: {e}") - return False - return True - +class FolderStatusResponse(BaseModel): + folder_id: str + status: str # 'completed', 'in_progress', 'pending' + total_images: int + tagged_images: int + progress_percentage: float -def post_sync_folder_sequence( - folder_path: str, folder_id: int, added_folders: List[Tuple[str, str]] +@router.post("/folders/bulk-tag") +async def bulk_tag_folders( + request: BulkTagRequest, + background_tasks: BackgroundTasks ): """ - Post-sync sequence for a folder. - This function is called after a folder is synced. - It processes images in the folder and updates the database. + Enable AI tagging for multiple folders at once. + Processes folders in batches to avoid overwhelming the system. """ - try: - # Create folder data array - folder_data = [] - - folder_data.append((folder_path, folder_id, False)) - - for added_folder_id, added_folder_path in added_folders: - folder_data.append((added_folder_path, added_folder_id, False)) - - logger.info(f"Sync folder: {folder_data}") - # Process images in all folders - image_util_process_folder_images(folder_data) - image_util_process_untagged_images() - cluster_util_face_clusters_sync() - - # Restart sync microservice watcher after processing images - API_util_restart_sync_microservice_watcher() - except Exception as e: - logger.error( - f"Error in post processing after folder {folder_path} was synced: {e}" - ) - return False - return True - - -def get_state(request: Request): - return request.app.state - - -@router.post( - "/add-folder", - response_model=AddFolderResponse, - responses={code: {"model": ErrorResponse} for code in [400, 401, 409, 500]}, -) -def add_folder(request: AddFolderRequest, app_state=Depends(get_state)): - try: - # Step 1: Data Validation - - if not os.path.isdir(request.folder_path): - raise ValueError( - f"Error: '{request.folder_path}' is not a valid directory." - ) - - if ( - not os.access(request.folder_path, os.R_OK) - # Uncomment the following lines if you want to check for write and execute permissions - # or not os.access(request.folder_path, os.W_OK) - # or not os.access(request.folder_path, os.X_OK) - ): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail=ErrorResponse( - success=False, - error="Permission denied", - message="The app does not have read permission for the specified folder", - ).model_dump(), - ) - - request.folder_path = os.path.abspath(request.folder_path) - - # Step 2: Check if folder already exists - if db_folder_exists(request.folder_path): - raise HTTPException( - status_code=status.HTTP_409_CONFLICT, - detail=ErrorResponse( - success=False, - error="Folder Already Exists", - message=f"Folder '{request.folder_path}' is already in the database.", - ).model_dump(), - ) - - # Step 3: If parent_folder_id not provided, try to find it - parent_folder_id = request.parent_folder_id - if parent_folder_id is None: - parent_folder_id = db_find_parent_folder_id(request.folder_path) - - # Step 4: Add folder tree to database - root_folder_id, folder_map = folder_util_add_folder_tree( - root_path=request.folder_path, - parent_folder_id=parent_folder_id, - AI_Tagging=False, - taggingCompleted=request.taggingCompleted, - ) - - # Step 5: Update parent ids for the subtree - db_update_parent_ids_for_subtree(request.folder_path, folder_map) - - # Step 6: Call the post-addition sequence in a separate process - executor: ProcessPoolExecutor = app_state.executor - executor.submit(post_folder_add_sequence, request.folder_path, root_folder_id) - - return AddFolderResponse( - data=AddFolderData( - folder_id=root_folder_id, folder_path=request.folder_path - ), - success=True, - message=f"Successfully added folder tree starting at: {request.folder_path}", - ) - except ValueError as e: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ErrorResponse( - success=False, - error="Validation Error", - message=str(e), - ).model_dump(), - ) - except HTTPException as e: - # Re-raise HTTPExceptions to preserve the status code and detail - raise e - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=ErrorResponse( - success=False, - error="Internal server error", - message=f"Unable to add folder: {str(e)}", - ).model_dump(), - ) - - -@router.post( - "/enable-ai-tagging", - response_model=UpdateAITaggingResponse, - responses={code: {"model": ErrorResponse} for code in [400, 500]}, -) -def enable_ai_tagging(request: UpdateAITaggingRequest, app_state=Depends(get_state)): - """Enable AI tagging for multiple folders.""" - try: - if not request.folder_ids: - raise ValueError("No folder IDs provided") - - updated_count = db_enable_ai_tagging_batch(request.folder_ids) - - executor: ProcessPoolExecutor = app_state.executor - executor.submit(post_AI_tagging_enabled_sequence) - - return UpdateAITaggingResponse( - data=UpdateAITaggingData( - updated_count=updated_count, folder_ids=request.folder_ids - ), - success=True, - message=f"Successfully enabled AI tagging for {updated_count} folder(s)", - ) - - except ValueError as e: + if not request.folder_ids: + raise HTTPException(status_code=400, detail="No folders provided") + + if len(request.folder_ids) > 50: raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ErrorResponse( - success=False, - error="Validation Error", - message=str(e), - ).model_dump(), - ) - except Exception as e: + status_code=400, + detail="Maximum 50 folders can be tagged at once" + ) + + # Validate all folders exist + invalid_folders = [] + for folder_id in request.folder_ids: + folder = db_get_folder_by_id(folder_id) + if not folder: + invalid_folders.append(folder_id) + + if invalid_folders: raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=ErrorResponse( - success=False, - error="Internal server error", - message=f"Unable to enable AI tagging: {str(e)}", - ).model_dump(), - ) - - -@router.post( - "/disable-ai-tagging", - response_model=UpdateAITaggingResponse, - responses={code: {"model": ErrorResponse} for code in [400, 500]}, -) -def disable_ai_tagging(request: UpdateAITaggingRequest): - """Disable AI tagging for multiple folders.""" - try: - if not request.folder_ids: - raise ValueError("No folder IDs provided") - - updated_count = db_disable_ai_tagging_batch(request.folder_ids) - - return UpdateAITaggingResponse( - data=UpdateAITaggingData( - updated_count=updated_count, folder_ids=request.folder_ids - ), - success=True, - message=f"Successfully disabled AI tagging for {updated_count} folder(s)", - ) - - except ValueError as e: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ErrorResponse( - success=False, - error="Validation Error", - message=str(e), - ).model_dump(), - ) - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=ErrorResponse( - success=False, - error="Internal server error", - message=f"Unable to disable AI tagging: {str(e)}", - ).model_dump(), - ) - - -@router.delete( - "/delete-folders", - response_model=DeleteFoldersResponse, - responses={code: {"model": ErrorResponse} for code in [400, 500]}, -) -def delete_folders(request: DeleteFoldersRequest): - """Delete multiple folders by their IDs.""" - try: - if not request.folder_ids: - raise ValueError("No folder IDs provided") - - deleted_count = db_delete_folders_batch(request.folder_ids) - - return DeleteFoldersResponse( - data=DeleteFoldersData( - deleted_count=deleted_count, folder_ids=request.folder_ids - ), - success=True, - message=f"Successfully deleted {deleted_count} folder(s)", - ) - - except ValueError as e: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ErrorResponse( - success=False, - error="Validation Error", - message=str(e), - ).model_dump(), - ) - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=ErrorResponse( - success=False, - error="Internal server error", - message=f"Unable to delete folders: {str(e)}", - ).model_dump(), - ) - - -@router.post( - "/sync-folder", - response_model=SyncFolderResponse, - responses={code: {"model": ErrorResponse} for code in [400, 404, 500]}, -) -def sync_folder(request: SyncFolderRequest, app_state=Depends(get_state)): - """Sync a folder by comparing filesystem folders with database entries and removing extra DB entries.""" - try: - # Step 1: Get current state from both sources - db_child_folders = db_get_direct_child_folders(request.folder_id) - filesystem_folders = folder_util_get_filesystem_direct_child_folders( - request.folder_path - ) - - # Step 2: Compare and identify differences - filesystem_folder_set = set(filesystem_folders) - db_folder_paths = {folder_path for folder_id, folder_path in db_child_folders} - - folders_to_delete = db_folder_paths - filesystem_folder_set - folders_to_add = filesystem_folder_set - db_folder_paths - - # Step 3: Perform synchronization operations - deleted_count, deleted_folders = folder_util_delete_obsolete_folders( - db_child_folders, folders_to_delete - ) - added_count, added_folders_with_ids = folder_util_add_multiple_folder_trees( - folders_to_add, request.folder_id - ) - - # Extract just the paths for the API response - added_folders = [ - folder_path for folder_id, folder_path in added_folders_with_ids - ] - - executor: ProcessPoolExecutor = app_state.executor - executor.submit( - post_sync_folder_sequence, - request.folder_path, - request.folder_id, - added_folders_with_ids, - ) - # Step 4: Return comprehensive response - return SyncFolderResponse( - data=SyncFolderData( - deleted_count=deleted_count, - deleted_folders=deleted_folders, - added_count=added_count, - added_folders=added_folders, - folder_id=request.folder_id, - folder_path=request.folder_path, - ), - success=True, - message=f"Successfully synced folder. Added {added_count} folder(s), deleted {deleted_count} folder(s)", - ) - - except ValueError as e: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail=ErrorResponse( - success=False, - error="Validation Error", - message=str(e), - ).model_dump(), - ) - except HTTPException as e: - # Re-raise HTTPExceptions to preserve the status code and detail - raise e - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=ErrorResponse( - success=False, - error="Internal server error", - message=f"Unable to sync folder: {str(e)}", - ).model_dump(), - ) - - -@router.get( - "/all-folders", - response_model=GetAllFoldersResponse, - responses={code: {"model": ErrorResponse} for code in [500]}, -) -def get_all_folders(): - """Get details of all folders in the database.""" - try: - folder_details_raw = db_get_all_folder_details() - - # Convert raw tuples to FolderDetails objects - folders = [] - for folder_data in folder_details_raw: - ( - folder_id, - folder_path, - parent_folder_id, - last_modified_time, - ai_tagging, - tagging_completed, - ) = folder_data - folders.append( - FolderDetails( - folder_id=folder_id, - folder_path=folder_path, - parent_folder_id=parent_folder_id, - last_modified_time=last_modified_time, - AI_Tagging=ai_tagging, - taggingCompleted=tagging_completed, - ) - ) - - return GetAllFoldersResponse( - data=GetAllFoldersData(folders=folders, total_count=len(folders)), - success=True, - message=f"Successfully retrieved {len(folders)} folder(s)", - ) - - except Exception as e: - raise HTTPException( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - detail=ErrorResponse( - success=False, - error="Internal server error", - message=f"Unable to retrieve folders: {str(e)}", - ).model_dump(), - ) + status_code=404, + detail=f"Folders not found: {', '.join(invalid_folders)}" + ) + + # Process in background + background_tasks.add_task( + process_bulk_tagging, + request.folder_ids + ) + + return { + "message": f"Started AI tagging for {len(request.folder_ids)} folders", + "folder_ids": request.folder_ids, + "status": "processing" + } + +async def process_bulk_tagging(folder_ids: List[str]): + """ + Process AI tagging for multiple folders. + Handles batch processing with rate limiting. + """ + batch_size = 5 + for i in range(0, len(folder_ids), batch_size): + batch = folder_ids[i:i + batch_size] + + # Process batch concurrently + tasks = [enable_ai_tagging_for_folder(fid) for fid in batch] + await asyncio.gather(*tasks, return_exceptions=True) + + # Small delay between batches + if i + batch_size < len(folder_ids): + await asyncio.sleep(1) + +@router.get("/folders/{folder_id}/status", response_model=FolderStatusResponse) +async def get_folder_tagging_status(folder_id: str): + """ + Get detailed tagging status for a specific folder. + """ + folder = db_get_folder_by_id(folder_id) + if not folder: + raise HTTPException(status_code=404, detail="Folder not found") + + total_images = db_count_images_in_folder(folder_id) + tagged_images = db_count_tagged_images_in_folder(folder_id) + + if total_images == 0: + status = "empty" + progress = 0.0 + elif tagged_images == total_images: + status = "completed" + progress = 100.0 + elif tagged_images > 0: + status = "in_progress" + progress = (tagged_images / total_images) * 100 + else: + status = "pending" + progress = 0.0 + + return FolderStatusResponse( + folder_id=folder_id, + status=status, + total_images=total_images, + tagged_images=tagged_images, + progress_percentage=round(progress, 2) + ) + +@router.post("/folders/bulk-status") +async def get_bulk_folder_status(request: BulkTagRequest): + """ + Get status for multiple folders at once. + More efficient than individual requests. + """ + statuses = [] + for folder_id in request.folder_ids: + try: + status = await get_folder_tagging_status(folder_id) + statuses.append(status.dict()) + except HTTPException: + # Skip invalid folders + continue + + return {"statuses": statuses} diff --git a/frontend/src/components/BulkTaggingControls.tsx b/frontend/src/components/BulkTaggingControls.tsx new file mode 100644 index 000000000..89a2b95f6 --- /dev/null +++ b/frontend/src/components/BulkTaggingControls.tsx @@ -0,0 +1,107 @@ +import React, { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Card } from '@/components/ui/card'; +import { Folder, CheckSquare, Tag, Loader2 } from 'lucide-react'; +import { useAppDispatch, useAppSelector } from '@/app/hooks'; +import { + selectAllFolders, + deselectAllFolders, + toggleFolderSelection, +} from '@/features/bulkTagging/bulkTaggingSlice'; + +interface BulkTaggingControlsProps { + onBulkTag: (folderIds: string[]) => Promise; + folders: Array<{ id: string; name: string; aiTaggingEnabled: boolean }>; +} + +export const BulkTaggingControls: React.FC = ({ + onBulkTag, + folders, +}) => { + const dispatch = useAppDispatch(); + const selectedFolderIds = useAppSelector( + (state) => state.bulkTagging.selectedFolderIds + ); + const [isTagging, setIsTagging] = useState(false); + + const allSelected = + folders.length > 0 && selectedFolderIds.length === folders.length; + + const handleSelectAll = () => { + if (allSelected) { + dispatch(deselectAllFolders()); + } else { + dispatch(selectAllFolders(folders.map((f) => f.id))); + } + }; + + const handleBulkTagAll = async () => { + setIsTagging(true); + try { + await onBulkTag(folders.map((f) => f.id)); + } finally { + setIsTagging(false); + } + }; + + const handleTagSelected = async () => { + if (selectedFolderIds.length === 0) return; + setIsTagging(true); + try { + await onBulkTag(selectedFolderIds); + } finally { + setIsTagging(false); + } + }; + + return ( + +
+
+ + +
+ +
+ + + +
+
+
+ ); +}; diff --git a/frontend/src/components/ProgressSummary.tsx b/frontend/src/components/ProgressSummary.tsx new file mode 100644 index 000000000..0fb2371b3 --- /dev/null +++ b/frontend/src/components/ProgressSummary.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { Card } from '@/components/ui/card'; +import { Progress } from '@/components/ui/progress'; +import { CheckCircle2, Clock, AlertCircle } from 'lucide-react'; + +interface ProgressSummaryProps { + totalFolders: number; + completedFolders: number; + inProgressFolders: number; + pendingFolders: number; +} + +export const ProgressSummary: React.FC = ({ + totalFolders, + completedFolders, + inProgressFolders, + pendingFolders, +}) => { + const completionPercentage = + totalFolders > 0 ? Math.round((completedFolders / totalFolders) * 100) : 0; + + return ( + +
+
+

AI Tagging Progress

+ + {completionPercentage}% + +
+ + + +
+
+ +
+

Completed

+

{completedFolders}

+
+
+ +
+ +
+

In Progress

+

{inProgressFolders}

+
+
+ +
+ +
+

Pending

+

{pendingFolders}

+
+
+
+ +

+ {completedFolders} of {totalFolders} folders tagged +

+
+
+ ); +}; diff --git a/frontend/src/features/bulkTagging/bulkTaggingSlice.ts b/frontend/src/features/bulkTagging/bulkTaggingSlice.ts new file mode 100644 index 000000000..d13c852c4 --- /dev/null +++ b/frontend/src/features/bulkTagging/bulkTaggingSlice.ts @@ -0,0 +1,56 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +interface BulkTaggingState { + selectedFolderIds: string[]; + isProcessing: boolean; + lastError: string | null; +} + +const initialState: BulkTaggingState = { + selectedFolderIds: [], + isProcessing: false, + lastError: null, +}; + +const bulkTaggingSlice = createSlice({ + name: 'bulkTagging', + initialState, + reducers: { + toggleFolderSelection: (state, action: PayloadAction) => { + const folderId = action.payload; + const index = state.selectedFolderIds.indexOf(folderId); + if (index > -1) { + state.selectedFolderIds.splice(index, 1); + } else { + state.selectedFolderIds.push(folderId); + } + }, + selectAllFolders: (state, action: PayloadAction) => { + state.selectedFolderIds = action.payload; + }, + deselectAllFolders: (state) => { + state.selectedFolderIds = []; + }, + setProcessing: (state, action: PayloadAction) => { + state.isProcessing = action.payload; + }, + setError: (state, action: PayloadAction) => { + state.lastError = action.payload; + }, + clearSelections: (state) => { + state.selectedFolderIds = []; + state.lastError = null; + }, + }, +}); + +export const { + toggleFolderSelection, + selectAllFolders, + deselectAllFolders, + setProcessing, + setError, + clearSelections, +} = bulkTaggingSlice.actions; + +export default bulkTaggingSlice.reducer; diff --git a/landing-page/index.html b/landing-page/index.html index 2816d4e84..4162bd2bd 100644 --- a/landing-page/index.html +++ b/landing-page/index.html @@ -2,9 +2,10 @@ - + - Vite + React + TS + + PictoPy - Intelligent Photo Gallery Management
diff --git a/landing-page/src/Pages/Landing page/Home1.tsx b/landing-page/src/Pages/Landing page/Home1.tsx index 3483b722c..7c07409a7 100644 --- a/landing-page/src/Pages/Landing page/Home1.tsx +++ b/landing-page/src/Pages/Landing page/Home1.tsx @@ -2,6 +2,17 @@ import { motion } from "framer-motion"; import { useEffect, useRef, useState } from "react"; const ShuffleHero = () => { + // Function to scroll to downloads section + const scrollToDownloads = () => { + const downloadsSection = document.getElementById('downloads-section'); + if (downloadsSection) { + downloadsSection.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + } + }; + return (
@@ -33,7 +44,9 @@ const ShuffleHero = () => {
+ {/* Download button - scrolls to downloads section */} { Download - {/* Update this button to navigate to the GitHub link */} + {/* View Docs button - links to documentation */} { - // State for showing the notification const [downloadStarted, setDownloadStarted] = useState(null); + const [downloadLinks, setDownloadLinks] = useState<{ + mac: string | null; + windows: string | null; + linux: string | null; + }>({ mac: null, windows: null, linux: null }); + const [loading, setLoading] = useState(true); + + // Fetch latest release from GitHub API + useEffect(() => { + const fetchLatestRelease = async () => { + try { + const response = await fetch( + 'https://api.github.com/repos/AOSSIE-Org/PictoPy/releases/latest' + ); + const data: GitHubRelease = await response.json(); + + // Find download links for each platform + const macAsset = data.assets.find((asset) => + asset.name.toLowerCase().includes('dmg') || + asset.name.toLowerCase().includes('macos') + ); + + const windowsAsset = data.assets.find((asset) => + asset.name.toLowerCase().includes('exe') || + asset.name.toLowerCase().includes('.msi') + ); + + const linuxAsset = data.assets.find((asset) => + asset.name.toLowerCase().includes('.deb') + ); + + setDownloadLinks({ + mac: macAsset?.browser_download_url || null, + windows: windowsAsset?.browser_download_url || null, + linux: linuxAsset?.browser_download_url || null, + }); + setLoading(false); + } catch (error) { + console.error('Error fetching latest release:', error); + setLoading(false); + } + }; + + fetchLatestRelease(); + }, []); - // Function to handle button click and show the notification - const handleDownloadClick = (platform: string) => { + // Function to handle button click and show notification + const handleDownloadClick = (platform: string, downloadUrl: string | null) => { + if (!downloadUrl) { + setDownloadStarted(`${platform} download not available yet`); + setTimeout(() => setDownloadStarted(null), 3000); + return; + } + + // Trigger download + window.location.href = downloadUrl; + setDownloadStarted(`Download for ${platform} started!`); - // Hide the notification after 3 seconds setTimeout(() => { setDownloadStarted(null); }, 3000); }; return ( -
+
{/* Background Animated SVG */}
{
{/* Content */} -
+
{/* Heading with Gradient Text and Logo */}
@@ -66,14 +131,22 @@ const PictopyLanding: FC = () => { Organize your photos effortlessly. Available for Mac, Windows, and Linux.

+ {/* Loading State */} + {loading && ( +
+ Loading latest releases... +
+ )} + {/* Download Buttons */}