87 lines
2.9 KiB
React
87 lines
2.9 KiB
React
import { useEffect, useRef, useState } from 'react'
|
|
import { imgUrl } from '../api'
|
|
|
|
// True justified layout: pack photos greedily into rows at their real aspect
|
|
// ratios, then scale each row's height so it fills the container width
|
|
// exactly. No cropping, no stretch, and the last row simply renders at the
|
|
// target height instead of being padded by a spacer.
|
|
function layoutRows(photos, containerWidth, targetHeight, gap) {
|
|
const rows = []
|
|
let row = []
|
|
let arSum = 0
|
|
let index = 0
|
|
for (const photo of photos) {
|
|
const ar = photo.width && photo.height ? photo.width / photo.height : 1.5
|
|
row.push({ photo, ar, index: index++ })
|
|
arSum += ar
|
|
const gaps = (row.length - 1) * gap
|
|
if (arSum * targetHeight + gaps >= containerWidth) {
|
|
rows.push({ items: row, height: (containerWidth - gaps) / arSum })
|
|
row = []
|
|
arSum = 0
|
|
}
|
|
}
|
|
if (row.length > 0) {
|
|
const gaps = (row.length - 1) * gap
|
|
rows.push({
|
|
items: row,
|
|
height: Math.min(targetHeight, (containerWidth - gaps) / arSum),
|
|
})
|
|
}
|
|
return rows
|
|
}
|
|
|
|
export default function Gallery({ photos, onOpen, overlay, selected, onToggleSelect }) {
|
|
const containerRef = useRef(null)
|
|
const [width, setWidth] = useState(0)
|
|
|
|
useEffect(() => {
|
|
const el = containerRef.current
|
|
if (!el) return
|
|
const observer = new ResizeObserver((entries) => setWidth(entries[0].contentRect.width))
|
|
observer.observe(el)
|
|
return () => observer.disconnect()
|
|
}, [])
|
|
|
|
if (photos.length === 0) return null
|
|
const gap = 6
|
|
const targetHeight = width < 700 ? 170 : 240
|
|
const rows = width > 0 ? layoutRows(photos, width, targetHeight, gap) : []
|
|
const selecting = selected && selected.size > 0
|
|
|
|
return (
|
|
<div ref={containerRef} className={`gallery${selecting ? ' selecting' : ''}`}>
|
|
{rows.map((row) => (
|
|
<div key={row.items[0].photo.id} className="g-row" style={{ height: row.height }}>
|
|
{row.items.map(({ photo: p, ar, index }) => {
|
|
const isSelected = selected ? selected.has(p.id) : false
|
|
return (
|
|
<div
|
|
key={p.id}
|
|
className={`g-item${isSelected ? ' selected' : ''}`}
|
|
style={{ width: ar * row.height }}
|
|
onClick={() => onOpen && onOpen(index)}
|
|
>
|
|
<img src={imgUrl(p, 'thumb')} loading="lazy" alt={p.filename} />
|
|
{onToggleSelect && (
|
|
<button
|
|
className="g-check"
|
|
title={isSelected ? 'Deselect' : 'Select'}
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
onToggleSelect(p.id)
|
|
}}
|
|
>
|
|
✓
|
|
</button>
|
|
)}
|
|
{overlay && overlay(p)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|