diff --git a/photo21/static/layout.css b/photo21/static/layout.css index a0026d1..8e5568e 100644 --- a/photo21/static/layout.css +++ b/photo21/static/layout.css @@ -45,6 +45,29 @@ SPDX-License-Identifier: GPL-3.0-or-later background-color: rgba(163, 163, 163, 0.274); } +/* Gallery - Google Photos style justified grid */ +#lightgallery { + position: relative; +} + +.photo-item { + position: absolute; + overflow: hidden; + visibility: hidden; +} + +.photo-item img { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.photo-item img.photo-private { + outline: 5px solid var(--bs-danger); + outline-offset: -5px; +} + /* Language selector */ .lang-select { border: none; diff --git a/photologue/migrations/0008_photo_dimensions.py b/photologue/migrations/0008_photo_dimensions.py new file mode 100644 index 0000000..033f144 --- /dev/null +++ b/photologue/migrations/0008_photo_dimensions.py @@ -0,0 +1,45 @@ +import photologue.models +from django.db import migrations, models + + +def fill_dimensions(apps, schema_editor): + Photo = apps.get_model('photologue', 'Photo') + for photo in Photo.objects.filter(image_width__isnull=True).iterator(): + try: + photo.image_width = photo.image.width + photo.image_height = photo.image.height + photo.save(update_fields=['image_width', 'image_height']) + except Exception: + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('photologue', '0007_allow_duplicate_photo_titles'), + ] + + operations = [ + migrations.AddField( + model_name='photo', + name='image_width', + field=models.PositiveIntegerField(editable=False, null=True), + ), + migrations.AddField( + model_name='photo', + name='image_height', + field=models.PositiveIntegerField(editable=False, null=True), + ), + migrations.RunPython(fill_dimensions, migrations.RunPython.noop), + migrations.AlterField( + model_name='photo', + name='image', + field=models.ImageField( + max_length=100, + upload_to=photologue.models.get_storage_path, + verbose_name='image', + width_field='image_width', + height_field='image_height', + ), + ), + ] diff --git a/photologue/static/gallery_justified.js b/photologue/static/gallery_justified.js new file mode 100644 index 0000000..80dcc55 --- /dev/null +++ b/photologue/static/gallery_justified.js @@ -0,0 +1,75 @@ +// Justified photo grid layout (Google Photos style) +// Inspired by https://github.com/flickr/justified-layout +(function () { + const SPACING = 3; + const PADDING = 3; + + function applyLayout() { + const container = document.getElementById('lightgallery'); + if (!container) return; + + const items = [...container.querySelectorAll('.photo-item')]; + if (!items.length) return; + + const containerWidth = container.offsetWidth - PADDING * 2; + const TARGET_HEIGHT = Math.min(220, Math.floor(containerWidth * 0.4)); + const ratios = items.map(a => { + const w = parseInt(a.dataset.width) || 1; + const h = parseInt(a.dataset.height) || 1; + return w / h; + }); + + // Group items into rows + const rows = []; + let rowStart = 0; + while (rowStart < ratios.length) { + let sum = 0; + let end = rowStart; + while (end < ratios.length) { + sum += ratios[end]; + const rowWidth = sum * TARGET_HEIGHT + SPACING * (end - rowStart); + end++; + if (rowWidth >= containerWidth) break; + } + rows.push({ start: rowStart, end }); + rowStart = end; + } + + // Position each item + let top = PADDING; + rows.forEach(({ start, end }, rowIndex) => { + const isLastRow = rowIndex === rows.length - 1; + const count = end - start; + const sumRatios = ratios.slice(start, end).reduce((a, b) => a + b, 0); + const totalSpacing = SPACING * (count - 1); + + // Don't stretch the last row if it's not full + const rowHeight = isLastRow && count < 3 + ? TARGET_HEIGHT + : (containerWidth - totalSpacing) / sumRatios; + + let left = PADDING; + for (let i = start; i < end; i++) { + const width = Math.round(ratios[i] * rowHeight); + const height = Math.round(rowHeight); + const item = items[i]; + item.style.top = top + 'px'; + item.style.left = left + 'px'; + item.style.width = width + 'px'; + item.style.height = height + 'px'; + left += width + SPACING; + } + top += Math.round(rowHeight) + SPACING; + }); + + container.style.height = (top - SPACING + PADDING) + 'px'; + items.forEach(item => { + item.style.visibility = 'visible'; + const img = item.querySelector('img[data-lazy]'); + if (img) img.src = img.dataset.lazy; + }); + } + + document.addEventListener('DOMContentLoaded', applyLayout); + window.addEventListener('resize', applyLayout); +})(); diff --git a/photologue/templates/photologue/gallery_detail.html b/photologue/templates/photologue/gallery_detail.html index 1744a05..155f86a 100755 --- a/photologue/templates/photologue/gallery_detail.html +++ b/photologue/templates/photologue/gallery_detail.html @@ -25,6 +25,7 @@ SPDX-License-Identifier: GPL-3.0-or-later + @@ -87,10 +88,10 @@ SPDX-License-Identifier: GPL-3.0-or-later {% endif %} -