Skip to content

Commit 7f7012f

Browse files
feat: enhance Memories UI with improved titles and event bubbling fix
✨ Features: - Display 'On this day last year' for memories from exactly 1 year ago - Format location-based memories as 'Trip to [Location], [Year]' (e.g., 'Trip to Jaipur, 2025') - Fix MediaView slideshow and info buttons not working in memory albums 🐛 Bug Fixes: - Fixed event bubbling issue where MediaView control clicks closed the entire viewer - Conditionally render MemoryViewer backdrop only when MediaView is closed - Prevent click handlers from interfering with MediaView controls 🎨 UI Improvements: - Enhanced FeaturedMemoryCard with contextual year display - Updated MemoryCard title formatting for better location context - Improved memory viewing experience with proper z-index layering 📦 Technical Changes: - Backend: Added reverse geocoding for location names in memory clustering - Backend: Fixed latitude/longitude handling for images without GPS data - Frontend: Refactored MemoryViewer JSX structure for proper conditional rendering - Frontend: Integrated MediaView component with full zoom/slideshow/info functionality This commit completes the Memories feature implementation with Google Photos-style presentation and fixes critical UX issues with the image viewer controls.
1 parent 109b438 commit 7f7012f

17 files changed

Lines changed: 2565 additions & 80 deletions

File tree

backend/app/database/images.py

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,16 +176,19 @@ def db_bulk_insert_images(image_records: List[ImageRecord]) -> bool:
176176
try:
177177
cursor.executemany(
178178
"""
179-
INSERT INTO images (id, path, folder_id, thumbnailPath, metadata, isTagged)
180-
VALUES (:id, :path, :folder_id, :thumbnailPath, :metadata, :isTagged)
179+
INSERT INTO images (id, path, folder_id, thumbnailPath, metadata, isTagged, latitude, longitude, captured_at)
180+
VALUES (:id, :path, :folder_id, :thumbnailPath, :metadata, :isTagged, :latitude, :longitude, :captured_at)
181181
ON CONFLICT(path) DO UPDATE SET
182182
folder_id=excluded.folder_id,
183183
thumbnailPath=excluded.thumbnailPath,
184184
metadata=excluded.metadata,
185185
isTagged=CASE
186186
WHEN excluded.isTagged THEN 1
187187
ELSE images.isTagged
188-
END
188+
END,
189+
latitude=COALESCE(excluded.latitude, images.latitude),
190+
longitude=COALESCE(excluded.longitude, images.longitude),
191+
captured_at=COALESCE(excluded.captured_at, images.captured_at)
189192
""",
190193
image_records,
191194
)
@@ -804,6 +807,67 @@ def db_get_images_with_location() -> List[dict]:
804807

805808
return images
806809

810+
except Exception as e:
811+
logger.error(f"Error fetching images with location: {e}")
812+
return []
813+
finally:
814+
conn.close()
815+
816+
817+
def db_get_all_images_for_memories() -> List[dict]:
818+
"""
819+
Get ALL images that can be used for memories (with OR without GPS).
820+
Includes images with timestamps for date-based memories.
821+
822+
Returns:
823+
List of all image dictionaries (both GPS and non-GPS images)
824+
"""
825+
conn = _connect()
826+
cursor = conn.cursor()
827+
828+
try:
829+
cursor.execute("""
830+
SELECT
831+
i.id,
832+
i.path,
833+
i.folder_id,
834+
i.thumbnailPath,
835+
i.metadata,
836+
i.isTagged,
837+
i.isFavourite,
838+
i.latitude,
839+
i.longitude,
840+
i.captured_at,
841+
GROUP_CONCAT(m.name, ',') as tags
842+
FROM images i
843+
LEFT JOIN image_classes ic ON i.id = ic.image_id
844+
LEFT JOIN mappings m ON ic.class_id = m.class_id
845+
GROUP BY i.id
846+
ORDER BY i.captured_at DESC
847+
""")
848+
849+
results = cursor.fetchall()
850+
851+
images = []
852+
for row in results:
853+
from app.utils.images import image_util_parse_metadata
854+
855+
images.append({
856+
"id": row[0],
857+
"path": row[1],
858+
"folder_id": str(row[2]) if row[2] else None,
859+
"thumbnailPath": row[3],
860+
"metadata": image_util_parse_metadata(row[4]),
861+
"isTagged": bool(row[5]),
862+
"isFavourite": bool(row[6]),
863+
"latitude": row[7] if row[7] else None, # Can be None
864+
"longitude": row[8] if row[8] else None, # Can be None
865+
"captured_at": row[9] if row[9] else None,
866+
"tags": row[10].split(',') if row[10] else None
867+
})
868+
869+
return images
870+
807871
except Exception as e:
808872
logger.error(f"Error getting images with location: {e}")
809873
return []

backend/app/routes/memories.py

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -116,46 +116,32 @@ async def generate_memories(
116116
min_images: int = Query(2, ge=1, le=10, description="Minimum images per memory")
117117
):
118118
"""
119-
Generate memories from all images with location data.
119+
SIMPLIFIED: Generate memories from ALL images.
120+
- GPS images → location-based memories
121+
- Non-GPS images → monthly date-based memories
120122
121-
This endpoint:
122-
1. Fetches all images that have GPS coordinates
123-
2. Clusters them by location using DBSCAN
124-
3. Within each location, clusters by date
125-
4. Returns memory objects with metadata
126-
127-
Args:
128-
location_radius_km: Maximum distance between photos in same location (default: 5km)
129-
date_tolerance_days: Maximum days between photos in same memory (default: 3)
130-
min_images: Minimum images required to form a memory (default: 2)
131-
132-
Returns:
133-
GenerateMemoriesResponse with list of generated memories
134-
135-
Raises:
136-
HTTPException: If database query fails or clustering fails
123+
Returns simple breakdown: {location_count, date_count, total}
137124
"""
138125
try:
139-
logger.info("Generating memories with params: "
140-
f"radius={location_radius_km}km, "
141-
f"date_tolerance={date_tolerance_days}days, "
142-
f"min_images={min_images}")
126+
logger.info(f"Generating memories: radius={location_radius_km}km, "
127+
f"date_tolerance={date_tolerance_days}days, min_images={min_images}")
143128

144-
# Fetch all images with location data
145-
images = db_get_images_with_location()
129+
# Fetch ALL images
130+
from app.database.images import db_get_all_images_for_memories
131+
images = db_get_all_images_for_memories()
146132

147133
if not images:
148134
return GenerateMemoriesResponse(
149135
success=True,
150-
message="No images with location data found",
136+
message="No images found",
151137
memory_count=0,
152138
image_count=0,
153139
memories=[]
154140
)
155141

156-
logger.info(f"Found {len(images)} images with location data")
142+
logger.info(f"Processing {len(images)} images")
157143

158-
# Cluster images into memories
144+
# Cluster into memories
159145
clustering = MemoryClustering(
160146
location_radius_km=location_radius_km,
161147
date_tolerance_days=date_tolerance_days,
@@ -164,19 +150,24 @@ async def generate_memories(
164150

165151
memories = clustering.cluster_memories(images)
166152

167-
logger.info(f"Generated {len(memories)} memories")
153+
# Calculate breakdown
154+
location_count = sum(1 for m in memories if m.get('type') == 'location')
155+
date_count = sum(1 for m in memories if m.get('type') == 'date')
156+
157+
logger.info(f"Generated {len(memories)} memories "
158+
f"(location: {location_count}, date: {date_count})")
168159

169160
return GenerateMemoriesResponse(
170161
success=True,
171-
message=f"Successfully generated {len(memories)} memories from {len(images)} images",
162+
message=f"{len(memories)} memories ({location_count} location, {date_count} date)",
172163
memory_count=len(memories),
173164
image_count=len(images),
174165
memories=memories
175166
)
176167

177168
except Exception as e:
178-
logger.error(f"Error generating memories: {e}")
179-
raise HTTPException(status_code=500, detail=f"Failed to generate memories: {str(e)}")
169+
logger.error(f"Error generating memories: {e}", exc_info=True)
170+
raise HTTPException(status_code=500, detail=str(e))
180171

181172

182173
@router.get("/timeline", response_model=TimelineResponse)

backend/app/utils/images.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import datetime
44
import json
55
import logging
6-
from typing import List, Tuple, Dict, Any, Mapping
6+
from typing import List, Tuple, Dict, Any, Mapping, Optional
77
from PIL import Image, ExifTags
88
from pathlib import Path
99

@@ -19,6 +19,7 @@
1919
from app.models.FaceDetector import FaceDetector
2020
from app.models.ObjectClassifier import ObjectClassifier
2121
from app.logging.setup_logging import get_logger
22+
from app.utils.extract_location_metadata import MetadataExtractor
2223

2324
logger = get_logger(__name__)
2425

@@ -141,6 +142,7 @@ def image_util_prepare_image_records(
141142
) -> List[Dict]:
142143
"""
143144
Prepare image records with thumbnails for database insertion.
145+
Automatically extracts GPS coordinates and capture datetime from metadata.
144146
145147
Args:
146148
image_files: List of image file paths
@@ -150,6 +152,8 @@ def image_util_prepare_image_records(
150152
List of image record dictionaries ready for database insertion
151153
"""
152154
image_records = []
155+
extractor = MetadataExtractor()
156+
153157
for image_path in image_files:
154158
folder_id = image_util_find_folder_id_for_image(image_path, folder_path_to_id)
155159

@@ -166,16 +170,40 @@ def image_util_prepare_image_records(
166170
if image_util_generate_thumbnail(image_path, thumbnail_path):
167171
metadata = image_util_extract_metadata(image_path)
168172
logger.debug(f"Extracted metadata for {image_path}: {metadata}")
169-
image_records.append(
170-
{
171-
"id": image_id,
172-
"path": image_path,
173-
"folder_id": folder_id,
174-
"thumbnailPath": thumbnail_path,
175-
"metadata": json.dumps(metadata),
176-
"isTagged": False,
177-
}
178-
)
173+
174+
# Automatically extract GPS coordinates and datetime from metadata
175+
# Don't fail upload if extraction fails
176+
metadata_json = json.dumps(metadata)
177+
latitude, longitude, captured_at = None, None, None
178+
179+
try:
180+
latitude, longitude, captured_at = extractor.extract_all(metadata_json)
181+
182+
# Log GPS extraction results
183+
if latitude and longitude:
184+
logger.info(f"GPS extracted for {os.path.basename(image_path)}: ({latitude}, {longitude})")
185+
if captured_at:
186+
logger.debug(f"Date extracted for {os.path.basename(image_path)}: {captured_at}")
187+
except Exception as e:
188+
logger.warning(f"GPS extraction failed for {os.path.basename(image_path)}: {e}")
189+
# Continue without GPS - don't fail the upload
190+
191+
# Build image record with GPS data
192+
# ALWAYS include latitude, longitude, captured_at (even if None)
193+
# to satisfy SQL INSERT statement named parameters
194+
image_record = {
195+
"id": image_id,
196+
"path": image_path,
197+
"folder_id": folder_id,
198+
"thumbnailPath": thumbnail_path,
199+
"metadata": metadata_json,
200+
"isTagged": False,
201+
"latitude": latitude, # Can be None
202+
"longitude": longitude, # Can be None
203+
"captured_at": captured_at.isoformat() if isinstance(captured_at, datetime.datetime) and captured_at else captured_at, # Can be None
204+
}
205+
206+
image_records.append(image_record)
179207

180208
return image_records
181209

0 commit comments

Comments
 (0)