feat: timeline and folder import
This commit is contained in:
@@ -4,14 +4,16 @@ import clsx from 'clsx'
|
||||
|
||||
interface Photo {
|
||||
id: string
|
||||
path: string
|
||||
filepath: string
|
||||
filename: string
|
||||
width: number
|
||||
height: number
|
||||
date_taken: string | null
|
||||
width: number | null
|
||||
height: number | null
|
||||
taken_at: string | null
|
||||
rating: number
|
||||
flag: string | null
|
||||
hash: string
|
||||
is_picked: boolean
|
||||
is_rejected: boolean
|
||||
file_hash: string
|
||||
media_type: string
|
||||
}
|
||||
|
||||
interface PhotoThumbnailProps {
|
||||
@@ -28,8 +30,8 @@ export function PhotoThumbnail({ photo, size, isSelected, onClick }: PhotoThumbn
|
||||
// Generate thumbnail URL - assuming backend serves thumbnails at /api/photos/{id}/thumbnail
|
||||
const thumbnailUrl = `http://localhost:8001/api/v1/photos/${photo.id}/thumb/medium`
|
||||
|
||||
// Calculate aspect ratio for proper sizing
|
||||
const aspectRatio = photo.height / photo.width
|
||||
// Calculate aspect ratio for proper sizing (default to 1:1 if dimensions unknown)
|
||||
const aspectRatio = (photo.height && photo.width) ? photo.height / photo.width : 1
|
||||
const displayHeight = size * Math.min(aspectRatio, 1.5) // Cap height at 1.5x width
|
||||
|
||||
const handleImageLoad = () => {
|
||||
@@ -49,7 +51,7 @@ export function PhotoThumbnail({ photo, size, isSelected, onClick }: PhotoThumbn
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'relative cursor-pointer overflow-hidden rounded-sm transition-all duration-200',
|
||||
'group relative cursor-pointer overflow-hidden rounded-sm transition-all duration-200',
|
||||
'hover:ring-2 hover:ring-primary/50',
|
||||
isSelected && 'ring-2 ring-primary shadow-lg',
|
||||
!imageLoaded && 'bg-surface animate-pulse'
|
||||
@@ -59,6 +61,7 @@ export function PhotoThumbnail({ photo, size, isSelected, onClick }: PhotoThumbn
|
||||
height: displayHeight,
|
||||
}}
|
||||
onClick={onClick}
|
||||
title="Click to select • Shift+Click for range • Ctrl+Click to add"
|
||||
>
|
||||
{/* Thumbnail Image */}
|
||||
{!imageError ? (
|
||||
@@ -102,22 +105,20 @@ export function PhotoThumbnail({ photo, size, isSelected, onClick }: PhotoThumbn
|
||||
)}
|
||||
|
||||
{/* Flag Indicators */}
|
||||
{photo.flag && (
|
||||
<div className="absolute bottom-1 right-1">
|
||||
{photo.flag === 'pick' && (
|
||||
<Check className="h-4 w-4 text-pick" />
|
||||
)}
|
||||
{photo.flag === 'reject' && (
|
||||
<X className="h-4 w-4 text-reject" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="absolute bottom-1 right-1">
|
||||
{photo.is_picked && (
|
||||
<Check className="h-4 w-4 text-pick" />
|
||||
)}
|
||||
{photo.is_rejected && (
|
||||
<X className="h-4 w-4 text-reject" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* File Type Badge for RAW/Video */}
|
||||
{(photo.path.toLowerCase().match(/\.(raw|arw|cr2|cr3|nef|orf|rw2|dng)$/i) ||
|
||||
photo.path.toLowerCase().match(/\.(mov|mp4|avi|mkv)$/i)) && (
|
||||
{(photo.filepath.toLowerCase().match(/\.(raw|arw|cr2|cr3|nef|orf|rw2|dng)$/i) ||
|
||||
photo.filepath.toLowerCase().match(/\.(mov|mp4|avi|mkv)$/i)) && (
|
||||
<div className="absolute right-1 top-1 rounded bg-black/50 px-1 py-0.5 text-[10px] font-medium text-white">
|
||||
{photo.path.toLowerCase().match(/\.(mov|mp4|avi|mkv)$/i) ? 'VIDEO' : 'RAW'}
|
||||
{photo.filepath.toLowerCase().match(/\.(mov|mp4|avi|mkv)$/i) ? 'VIDEO' : 'RAW'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -8,14 +8,16 @@ import axios from 'axios'
|
||||
|
||||
interface Photo {
|
||||
id: string
|
||||
path: string
|
||||
filepath: string
|
||||
filename: string
|
||||
width: number
|
||||
height: number
|
||||
date_taken: string | null
|
||||
width: number | null
|
||||
height: number | null
|
||||
taken_at: string | null
|
||||
rating: number
|
||||
flag: string | null
|
||||
hash: string
|
||||
is_picked: boolean
|
||||
is_rejected: boolean
|
||||
file_hash: string
|
||||
media_type: string
|
||||
}
|
||||
|
||||
export function Timeline() {
|
||||
@@ -61,13 +63,13 @@ export function Timeline() {
|
||||
const { data: photos = [], isLoading } = useQuery({
|
||||
queryKey: ['photos'],
|
||||
queryFn: async () => {
|
||||
const response = await axios.get<Photo[]>('http://localhost:8001/api/v1/photos', {
|
||||
const response = await axios.get<{photos: Photo[], total: number}>('http://localhost:8001/api/v1/photos', {
|
||||
params: {
|
||||
limit: 1000,
|
||||
offset: 0,
|
||||
},
|
||||
})
|
||||
return response.data
|
||||
return response.data.photos || []
|
||||
},
|
||||
staleTime: 30000,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user