Add justified photo grid layout with lazy loading and image dimensions.
This commit is contained in:
parent
3efa217716
commit
687445e414
4 changed files with 147 additions and 3 deletions
|
|
@ -45,6 +45,29 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
background-color: rgba(163, 163, 163, 0.274);
|
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 */
|
/* Language selector */
|
||||||
.lang-select {
|
.lang-select {
|
||||||
border: none;
|
border: none;
|
||||||
|
|
|
||||||
45
photologue/migrations/0008_photo_dimensions.py
Normal file
45
photologue/migrations/0008_photo_dimensions.py
Normal file
|
|
@ -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',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
75
photologue/static/gallery_justified.js
Normal file
75
photologue/static/gallery_justified.js
Normal file
|
|
@ -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);
|
||||||
|
})();
|
||||||
|
|
@ -25,6 +25,7 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
<script src="{% static 'lightgallery/plugins/hash/lg-hash.min.js' %}"></script>
|
<script src="{% static 'lightgallery/plugins/hash/lg-hash.min.js' %}"></script>
|
||||||
<script src="{% static 'lightgallery/plugins/thumbnail/lg-thumbnail.min.js' %}"></script>
|
<script src="{% static 'lightgallery/plugins/thumbnail/lg-thumbnail.min.js' %}"></script>
|
||||||
<script src="{% static 'lightgallery/plugins/zoom/lg-zoom.min.js' %}"></script>
|
<script src="{% static 'lightgallery/plugins/zoom/lg-zoom.min.js' %}"></script>
|
||||||
|
<script src="{% static 'gallery_justified.js' %}"></script>
|
||||||
<script src="{% static 'gallery_detail.js' %}"></script>
|
<script src="{% static 'gallery_detail.js' %}"></script>
|
||||||
<script src="{% static 'sweetalert.js' %}"></script>
|
<script src="{% static 'sweetalert.js' %}"></script>
|
||||||
<script src="{% static 'copy-button.js' %}"></script>
|
<script src="{% static 'copy-button.js' %}"></script>
|
||||||
|
|
@ -87,10 +88,10 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="card-body row" id="lightgallery">
|
<div class="card-body p-0" id="lightgallery">
|
||||||
{% for photo in photos %}
|
{% for photo in photos %}
|
||||||
<a class="col-6 col-md-3 mb-2 text-center" href="{{ photo.get_absolute_url }}" data-src="{{ photo.get_display_url}}" data-download-url="{{ photo.image.url }}" data-slide-name="{{ photo.id }}" data-owner-id="{{ photo.owner.id }}" data-is-public="{{ photo.is_public|yesno:'true,false' }}">
|
<a class="photo-item" href="{{ photo.get_absolute_url }}" data-src="{{ photo.get_display_url}}" data-download-url="{{ photo.image.url }}" data-slide-name="{{ photo.id }}" data-owner-id="{{ photo.owner.id }}" data-is-public="{{ photo.is_public|yesno:'true,false' }}" data-width="{{ photo.image_width|default:1 }}" data-height="{{ photo.image_height|default:1 }}">
|
||||||
<img src="{{ photo.get_thumbnail_url }}" loading="lazy" class="img-thumbnail p-0{% if not photo.is_public %} border-danger border-5{% endif %}" alt="{{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %}{% if photo.owner.get_full_name %} - {{ photo.owner.get_full_name }}{% else %} - {{ photo.owner.username }}{% endif %}{% if photo.license %} - {{ photo.license }}{% endif %}{% if not photo.is_public %} - !PRIVATE!{% endif %}">
|
<img data-lazy="{{ photo.get_thumbnail_url }}" class="{% if not photo.is_public %}photo-private{% endif %}" alt="{{ photo.title }}{% if photo.date_taken %} - {{ photo.date_taken|date }} {{ photo.date_taken|time }}{% endif %}{% if photo.owner.get_full_name %} - {{ photo.owner.get_full_name }}{% else %} - {{ photo.owner.username }}{% endif %}{% if photo.license %} - {{ photo.license }}{% endif %}{% if not photo.is_public %} - !PRIVATE!{% endif %}">
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue