diff --git a/frontend/src/api/pendingLinkages.ts b/frontend/src/api/pendingLinkages.ts index fb0f2b5..cc92958 100644 --- a/frontend/src/api/pendingLinkages.ts +++ b/frontend/src/api/pendingLinkages.ts @@ -15,6 +15,7 @@ export interface PendingLinkageResponse { updated_at: string | null photo_filename: string | null photo_path: string | null + photo_media_type: string | null photo_tags: string[] } diff --git a/frontend/src/api/reportedPhotos.ts b/frontend/src/api/reportedPhotos.ts index 07ae8ef..1f03bc7 100644 --- a/frontend/src/api/reportedPhotos.ts +++ b/frontend/src/api/reportedPhotos.ts @@ -14,6 +14,7 @@ export interface ReportedPhotoResponse { report_comment: string | null photo_path: string | null photo_filename: string | null + photo_media_type: string | null } export interface ReportedPhotosListResponse { diff --git a/frontend/src/pages/PendingPhotos.tsx b/frontend/src/pages/PendingPhotos.tsx index 04cc055..c32175f 100644 --- a/frontend/src/pages/PendingPhotos.tsx +++ b/frontend/src/pages/PendingPhotos.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useRef, useMemo } from 'react' import { pendingPhotosApi, PendingPhotoResponse, ReviewDecision, CleanupResponse } from '../api/pendingPhotos' import { apiClient } from '../api/client' import { useAuth } from '../context/AuthContext' +import { videosApi } from '../api/videos' type SortKey = 'photo' | 'uploaded_by' | 'file_info' | 'submitted_at' | 'status' @@ -625,22 +626,42 @@ export default function PendingPhotos() {
{ - // For full-size view, fetch as blob and open in new tab - try { - const blobUrl = imageUrls[photo.id] || await pendingPhotosApi.getPendingPhotoImageBlob(photo.id) - // Create a new window with the blob URL - const newWindow = window.open() - if (newWindow) { - newWindow.location.href = blobUrl + const isVideo = photo.mime_type?.startsWith('video/') + if (isVideo) { + // For videos, open the video file directly + const videoUrl = `${apiClient.defaults.baseURL}/api/v1/pending-photos/${photo.id}/image` + window.open(videoUrl, '_blank') + } else { + // For images, fetch as blob and open in new tab + try { + const blobUrl = imageUrls[photo.id] || await pendingPhotosApi.getPendingPhotoImageBlob(photo.id) + // Create a new window with the blob URL + const newWindow = window.open() + if (newWindow) { + newWindow.location.href = blobUrl + } + } catch (err) { + console.error('Failed to open full-size image:', err) + alert('Failed to load full-size image') } - } catch (err) { - console.error('Failed to open full-size image:', err) - alert('Failed to load full-size image') } }} - title="Click to open full photo" + title={photo.mime_type?.startsWith('video/') ? 'Click to open video' : 'Click to open full photo'} > - {imageUrls[photo.id] ? ( + {photo.mime_type?.startsWith('video/') ? ( +
+ + + +
+ VIDEO +
+
+ ) : imageUrls[photo.id] ? ( {photo.original_filename} { - const photoUrl = `${apiClient.defaults.baseURL}/api/v1/photos/${reported.photo_id}/image` - window.open(photoUrl, '_blank') + const isVideo = reported.photo_media_type === 'video' + const url = isVideo + ? videosApi.getVideoUrl(reported.photo_id) + : `${apiClient.defaults.baseURL}/api/v1/photos/${reported.photo_id}/image` + window.open(url, '_blank') }} - title="Click to open full photo" + title={reported.photo_media_type === 'video' ? 'Click to open video' : 'Click to open full photo'} > - {`Photo { - const target = e.target as HTMLImageElement - target.style.display = 'none' - const parent = target.parentElement - if (parent && !parent.querySelector('.error-fallback')) { - const fallback = document.createElement('div') - fallback.className = - 'text-gray-400 text-xs error-fallback' - fallback.textContent = `#${reported.photo_id}` - parent.appendChild(fallback) - } - }} - /> + {reported.photo_media_type === 'video' ? ( + {`Video { + const target = e.target as HTMLImageElement + target.style.display = 'none' + const parent = target.parentElement + if (parent && !parent.querySelector('.error-fallback')) { + const fallback = document.createElement('div') + fallback.className = + 'text-gray-400 text-xs error-fallback' + fallback.textContent = `#${reported.photo_id}` + parent.appendChild(fallback) + } + }} + /> + ) : ( + {`Photo { + const target = e.target as HTMLImageElement + target.style.display = 'none' + const parent = target.parentElement + if (parent && !parent.querySelector('.error-fallback')) { + const fallback = document.createElement('div') + fallback.className = + 'text-gray-400 text-xs error-fallback' + fallback.textContent = `#${reported.photo_id}` + parent.appendChild(fallback) + } + }} + /> + )}
) : (
Photo not found
diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx index 132acfc..c321bab 100644 --- a/frontend/src/pages/Search.tsx +++ b/frontend/src/pages/Search.tsx @@ -93,8 +93,11 @@ export default function Search() { page_size: pageSize, } - // Add media type filter if not 'all' - if (mediaType && mediaType !== 'all') { + // For "Photos without faces" search, always exclude videos + if (searchType === 'no_faces') { + params.media_type = 'image' + } else if (mediaType && mediaType !== 'all') { + // Add media type filter if not 'all' for other search types params.media_type = mediaType } diff --git a/frontend/src/pages/UserTaggedPhotos.tsx b/frontend/src/pages/UserTaggedPhotos.tsx index 9aa6272..0dd34ca 100644 --- a/frontend/src/pages/UserTaggedPhotos.tsx +++ b/frontend/src/pages/UserTaggedPhotos.tsx @@ -6,6 +6,7 @@ import { } from '../api/pendingLinkages' import { apiClient } from '../api/client' import { useAuth } from '../context/AuthContext' +import { videosApi } from '../api/videos' type DecisionValue = 'approve' | 'deny' @@ -440,28 +441,51 @@ export default function UserTaggedPhotos() {
{ - const url = `${apiClient.defaults.baseURL}/api/v1/photos/${linkage.photo_id}/image` + const isVideo = linkage.photo_media_type === 'video' + const url = isVideo + ? videosApi.getVideoUrl(linkage.photo_id) + : `${apiClient.defaults.baseURL}/api/v1/photos/${linkage.photo_id}/image` window.open(url, '_blank') }} - title="Open photo in new tab" + title={linkage.photo_media_type === 'video' ? 'Open video in new tab' : 'Open photo in new tab'} > - {`Photo { - const target = event.target as HTMLImageElement - target.style.display = 'none' - const parent = target.parentElement - if (parent && !parent.querySelector('.fallback-text')) { - const fallback = document.createElement('div') - fallback.className = 'fallback-text text-gray-400 text-xs text-center' - fallback.textContent = `#${linkage.photo_id}` - parent.appendChild(fallback) - } - }} - /> + {linkage.photo_media_type === 'video' ? ( + {`Video { + const target = event.target as HTMLImageElement + target.style.display = 'none' + const parent = target.parentElement + if (parent && !parent.querySelector('.fallback-text')) { + const fallback = document.createElement('div') + fallback.className = 'fallback-text text-gray-400 text-xs text-center' + fallback.textContent = `#${linkage.photo_id}` + parent.appendChild(fallback) + } + }} + /> + ) : ( + {`Photo { + const target = event.target as HTMLImageElement + target.style.display = 'none' + const parent = target.parentElement + if (parent && !parent.querySelector('.fallback-text')) { + const fallback = document.createElement('div') + fallback.className = 'fallback-text text-gray-400 text-xs text-center' + fallback.textContent = `#${linkage.photo_id}` + parent.appendChild(fallback) + } + }} + /> + )}
) : (
Photo not found
diff --git a/src/web/api/pending_linkages.py b/src/web/api/pending_linkages.py index bb99492..73e9d07 100644 --- a/src/web/api/pending_linkages.py +++ b/src/web/api/pending_linkages.py @@ -67,6 +67,7 @@ class PendingLinkageResponse(BaseModel): updated_at: Optional[str] = None photo_filename: Optional[str] = None photo_path: Optional[str] = None + photo_media_type: Optional[str] = None photo_tags: list[str] = Field(default_factory=list) @@ -221,6 +222,7 @@ def list_pending_linkages( updated_at=updated_at, photo_filename=photo.filename if photo else None, photo_path=photo.path if photo else None, + photo_media_type=photo.media_type if photo else None, photo_tags=photo_tags_map.get(row.photo_id, []), ) ) diff --git a/src/web/api/reported_photos.py b/src/web/api/reported_photos.py index 8fd31a7..e54108d 100644 --- a/src/web/api/reported_photos.py +++ b/src/web/api/reported_photos.py @@ -36,6 +36,7 @@ class ReportedPhotoResponse(BaseModel): # Photo details from main database photo_path: Optional[str] = None photo_filename: Optional[str] = None + photo_media_type: Optional[str] = None class ReportedPhotosListResponse(BaseModel): @@ -147,10 +148,12 @@ def list_reported_photos( # Get photo details from main database photo_path = None photo_filename = None + photo_media_type = None photo = main_db.query(Photo).filter(Photo.id == row.photo_id).first() if photo: photo_path = photo.path photo_filename = photo.filename + photo_media_type = photo.media_type items.append(ReportedPhotoResponse( id=row.id, @@ -166,6 +169,7 @@ def list_reported_photos( report_comment=row.report_comment, photo_path=photo_path, photo_filename=photo_filename, + photo_media_type=photo_media_type, )) return ReportedPhotosListResponse(items=items, total=len(items))