feat: pre-zip download all and photo replacement by name (#312, #313)

Pre-zip downloads:
- Generate ZIP in background after photo mutations (upload/delete/watermark change)
- Serve cached zip with Content-Length for instant downloads and native progress bar
- Falls back to on-the-fly streaming when no cache exists yet
- Frontend uses browser-native download when zip is ready (no blob buffering)
- New downloadZipService with debounced regeneration and in-memory locking

Photo replacement:
- Admin upload form gets "Replace existing photos with same name" checkbox
- Matches by original_filename (case-insensitive) within the same event
- Preserves photo ID, position, feedback, category, and visibility
- Updates file, thumbnail, dimensions, EXIF capture date on replacement
- Ambiguous matches (multiple photos with same name) skip replacement with warning
- New photoReplacementService with findReplacementCandidate and replacePhoto
This commit is contained in:
Paul Nothaft
2026-04-23 16:49:31 +02:00
parent 4353acebf9
commit e18afd3e6b
16 changed files with 664 additions and 36 deletions
+16 -2
View File
@@ -82,12 +82,26 @@ export const galleryService = {
},
// Download all photos as ZIP
async downloadAllPhotos(slug: string): Promise<void> {
// When a pre-generated zip is available, use native browser download (Content-Length → progress bar).
// Otherwise fall back to blob download.
async downloadAllPhotos(slug: string, zipReady?: boolean): Promise<void> {
if (zipReady) {
// Native browser download — the server sends Content-Length so
// the browser shows a real progress bar and mobile doesn't crash.
const link = document.createElement('a');
link.href = `/api/gallery/${slug}/download-all`;
link.setAttribute('download', `${slug}.zip`);
document.body.appendChild(link);
link.click();
link.remove();
return;
}
// Fallback: blob download (no Content-Length, buffered in memory)
const response = await api.get(`/gallery/${slug}/download-all`, {
responseType: 'blob',
});
// Create download link
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;