Rust (axum + sqlx) API and worker sharing a Postgres-backed job queue (SKIP LOCKED, heartbeat, reaper, typed statuses), S3 storage with derived keys and a fully private bucket, OIDC photographer login with per-request allowlist checks, client share links with argon2 passwords and lockout, cookie-based image authorization with sliding expiry, hand-rolled spec-compliant streaming ZIP downloads with exact Content-Length, React + Vite gallery frontend, single Docker image, Helm chart for external S3 + Postgres, and Gitea CI. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// Multi-select over a photo list. Selection lives here (not in the gallery)
|
||||
// so it survives lightbox open/close, and is pruned automatically when
|
||||
// photos disappear from the list (deletes, polling refreshes).
|
||||
export default function useSelection(photos) {
|
||||
const [selected, setSelected] = useState(() => new Set())
|
||||
|
||||
useEffect(() => {
|
||||
setSelected((prev) => {
|
||||
if (prev.size === 0) return prev
|
||||
const valid = new Set(photos.map((p) => p.id))
|
||||
const next = new Set([...prev].filter((id) => valid.has(id)))
|
||||
return next.size === prev.size ? prev : next
|
||||
})
|
||||
}, [photos])
|
||||
|
||||
const toggle = (photoId) =>
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(photoId)) next.delete(photoId)
|
||||
else next.add(photoId)
|
||||
return next
|
||||
})
|
||||
|
||||
const selectAll = () => setSelected(new Set(photos.map((p) => p.id)))
|
||||
const clear = () => setSelected(new Set())
|
||||
const selectedBytes = photos.reduce((sum, p) => sum + (selected.has(p.id) ? p.size_bytes : 0), 0)
|
||||
const totalBytes = photos.reduce((sum, p) => sum + p.size_bytes, 0)
|
||||
|
||||
return { selected, toggle, selectAll, clear, selectedBytes, totalBytes }
|
||||
}
|
||||
Reference in New Issue
Block a user