From 61f1bd147426b78d0d92f13c88e62cebcc898726 Mon Sep 17 00:00:00 2001 From: nils Date: Fri, 17 Jul 2026 16:30:26 +0200 Subject: [PATCH] Bulk delete, editable share links, gallery filter fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - shift-click selects ranges in the gallery; selection bar gains 'Delete N' with a single confirm (POST /api/photos/delete) - PATCH /api/shares/{id}: allow_download and expiry editable in place, token and client feedback preserved - Gallery: re-attach ResizeObserver via callback ref โ€” after a filter with zero matches the gallery stayed blank at width 0 --- frontend/src/components/Gallery.jsx | 20 +++++---- frontend/src/components/SelectionBar.jsx | 6 +++ frontend/src/pages/AlbumPage.jsx | 53 +++++++++++++++++++++--- frontend/src/useSelection.js | 18 +++++++- src/routes/mod.rs | 6 ++- src/routes/photos.rs | 29 +++++++++++++ src/routes/shares.rs | 43 +++++++++++++++++++ 7 files changed, 159 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/Gallery.jsx b/frontend/src/components/Gallery.jsx index 0f9ba4c..0d44b66 100644 --- a/frontend/src/components/Gallery.jsx +++ b/frontend/src/components/Gallery.jsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import { imgUrl } from '../api' // True justified layout: pack photos greedily into rows at their real aspect @@ -32,15 +32,19 @@ function layoutRows(photos, containerWidth, targetHeight, gap) { } export default function Gallery({ photos, onOpen, overlay, selected, onToggleSelect }) { - const containerRef = useRef(null) const [width, setWidth] = useState(0) + const observerRef = useRef(null) - useEffect(() => { - const el = containerRef.current + // Callback ref instead of mount effect: the container div unmounts whenever + // the photo list is empty (e.g. a filter with no matches), so the observer + // must re-attach to each new element โ€” a once-per-mount effect would leave + // the re-rendered gallery unobserved at width 0, rendering nothing. + const containerRef = useCallback((el) => { + observerRef.current?.disconnect() + observerRef.current = null if (!el) return - const observer = new ResizeObserver((entries) => setWidth(entries[0].contentRect.width)) - observer.observe(el) - return () => observer.disconnect() + observerRef.current = new ResizeObserver((entries) => setWidth(entries[0].contentRect.width)) + observerRef.current.observe(el) }, []) if (photos.length === 0) return null @@ -69,7 +73,7 @@ export default function Gallery({ photos, onOpen, overlay, selected, onToggleSel title={isSelected ? 'Deselect' : 'Select'} onClick={(e) => { e.stopPropagation() - onToggleSelect(p.id) + onToggleSelect(p.id, e.shiftKey) }} > โœ“ diff --git a/frontend/src/components/SelectionBar.jsx b/frontend/src/components/SelectionBar.jsx index 4004dfc..6c6e622 100644 --- a/frontend/src/components/SelectionBar.jsx +++ b/frontend/src/components/SelectionBar.jsx @@ -15,6 +15,7 @@ export default function SelectionBar({ onClear, onDownload, onDownloadAll, + onDelete, }) { if (total === 0) return null @@ -47,6 +48,11 @@ export default function SelectionBar({ + {onDelete && ( + + )} ) } diff --git a/frontend/src/pages/AlbumPage.jsx b/frontend/src/pages/AlbumPage.jsx index caa56a3..9559bb5 100644 --- a/frontend/src/pages/AlbumPage.jsx +++ b/frontend/src/pages/AlbumPage.jsx @@ -219,6 +219,22 @@ function SharesPanel({ albumId }) { setTimeout(() => setCopied(null), 1500) } + const update = async (shareId, patch) => { + try { + await api(`/api/shares/${shareId}`, { method: 'PATCH', body: patch }) + setError(null) + } catch (e) { + setError(e.message) + } + load() + } + + // ISO timestamp -> local yyyy-mm-dd for the date input. + const localDate = (iso) => { + const d = new Date(iso) + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` + } + return (

Client links

@@ -233,16 +249,34 @@ function SharesPanel({ albumId }) { {s.has_password ? '๐Ÿ”’ password' : 'no password'} {' ยท '} - {s.allow_download ? 'downloads on' : 'downloads off'} - {s.expires_at - ? ` ยท expires ${new Date(s.expires_at).toLocaleDateString()}` - : ' ยท never expires'} - {' ยท '} {s.rating_count} ratings, {s.tag_count} tags, ๐Ÿ‘ {s.accept_count} ๐Ÿ‘Ž{' '} {s.reject_count}
+ + {s.locked && (