feat: add justified/rows layout mode to masonry gallery (#146)

Add Google Photos-style justified row layout as a mode within masonry:

- Add masonryMode setting: 'columns' (Pinterest) or 'rows' (Google Photos)
- Create justifiedLayoutCalculator utility for row-based layouts
- Extract and store image dimensions on upload for layout calculations
- Include width/height in gallery API response
- Add row height and last row behavior controls to theme customizer
- Support responsive container width detection with ResizeObserver

Photos in rows mode maintain their aspect ratios while filling
horizontal rows at a consistent height. The number of photos per
row is automatically calculated based on target row height and
photo dimensions.

Closes #146
This commit is contained in:
Paul Nothaft
2026-01-27 09:58:09 +01:00
parent 85170b883f
commit e081b56a44
10 changed files with 666 additions and 20 deletions
+22 -1
View File
@@ -158,6 +158,7 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ
// Generate thumbnail and extract metadata
let thumbnailPath;
let videoMetadata = null;
let imageMetadata = null;
if (isVideo) {
// Process video: extract metadata and generate thumbnail
@@ -169,8 +170,22 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ
videoMetadata = result.metadata;
thumbnailPath = path.relative(getStoragePath(), videoThumbnailPath);
} else {
// Process image: generate thumbnail
// Process image: generate thumbnail and extract dimensions
thumbnailPath = await generateThumbnail(newPath);
// Extract image dimensions using sharp
try {
const sharp = require('sharp');
const metadata = await sharp(newPath).metadata();
if (metadata.width && metadata.height) {
imageMetadata = {
width: metadata.width,
height: metadata.height
};
}
} catch (metadataError) {
console.warn(`Could not extract image dimensions for ${file.originalname}:`, metadataError.message);
}
}
// Calculate relative paths
@@ -206,6 +221,12 @@ async function processUploadedPhotos(files, eventId, uploadedBy = 'admin', categ
photoData.height = videoMetadata.height;
}
// Add image dimensions if available
if (!isVideo && imageMetadata) {
photoData.width = imageMetadata.width;
photoData.height = imageMetadata.height;
}
if (supportsReturning) {
insertResult = await trx('photos')
.insert(photoData)