Skip to content

Commit

Permalink
issue Add swipping galery photos on mobile #164: better version
Browse files Browse the repository at this point in the history
  • Loading branch information
leolivier committed Dec 22, 2024
1 parent a8c3fca commit 9be561d
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 58 deletions.
7 changes: 7 additions & 0 deletions cm_main/templatetags/cm_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,10 @@ def startswith(text, starts):
def absolute_url(context, relative_url):
request = context['request']
return request.get_absolute_uri(relative_url)


@register.simple_tag
def get_absolute_idx_from_page(page, idx):
page_num = page.number
page_size = page.paginator.per_page
return (page_num - 1) * page_size + idx
196 changes: 141 additions & 55 deletions galleries/templates/galleries/gallery_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{% load i18n galleries_tags static cm_tags %}
{% block title %}{% title _("Show Gallery") %} {% endblock %}
{%block header %}
{%url 'galleries:gallery_photo_url' gallery.id '1234567890' as gallery_photo_url %}
<style>
#fullscreen-overlay {
display: none;
Expand All @@ -16,12 +17,33 @@
align-items: center;
}

#fullscreen-overlay img {
max-width: 95%;
max-height: 95%;
object-fit: contain;
.image-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transition: transform 0.3s ease-out;
display: flex;
align-items: center;
justify-content: center;
}


.image-container {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}

.image-container img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
margin: auto;
}

#close-fullscreen {
position: absolute;
top: 20px;
Expand All @@ -31,6 +53,7 @@
border: none;
padding: 10px 20px;
cursor: pointer;
z-index: 1001;
}

.navigation-arrow {
Expand Down Expand Up @@ -59,66 +82,123 @@
#next-image {
right: 20px;
}
</style>
</style>
<script>
let currentImage = null;
let fullscreenContainer = null;
// Variables to store touch start and end positions for swiping
let touchStartX = 0;
let touchEndX = 0;
const minSwipeDistance = 50; // Minimum distance to detect a swipe
const screenWidth = window.innerWidth;

// Function to open image in full screen
// Function to open image in full screen
function openFullscreen(imageElement) {
currentImage = imageElement;
let imageSrc = imageElement.data('fullscreen');
$('#fullscreen-image').attr('src', imageSrc); // set the image
$('#fullscreen-overlay') // display the overlay
// set the src of the current, next and previous images
let currentSrc = imageElement.data('fullscreen');
let nextSrc = imageElement.data('next');
let prevSrc = imageElement.data('prev');
let currentIdx = imageElement.data('idx');

$('.image-wrapper.prev img').attr('src', prevSrc);
$('.image-wrapper.current img').attr('src', currentSrc);
$('.image-wrapper.next img').attr('src', nextSrc);
// Update the current image IDX
fullscreenContainer.data('current-image-idx', currentIdx)
fullscreenContainer // display the overlay
.css('display', 'flex')
.hide()
.fadeIn(300);
}

function open_fullscreen_page(pageUrl, imageSrc) {
if (pageUrl) {
$('#fullscreen-image').attr('src', imageSrc);
window.location.href = pageUrl + '?openFullscreen=true&firstImage=' + imageSrc;
}
function getPhotoUrl(photoidx) {
return '{{ gallery_photo_url }}'.replace('1234567890', photoidx);
}

// Navigate to given page if any and in full screen mode
function goto_image(direction, pageUrl) {
if (currentImage) {
let imageSrc = currentImage.data(direction);
if (imageSrc) {
nextImage = $(`.gallery-image[data-fullscreen="${imageSrc}"]`);
if (nextImage.length === 0) { // must go to next/prev page
open_fullscreen_page(pageUrl, imageSrc);
} else {
currentImage = nextImage;
$('#fullscreen-image').attr('src', imageSrc);
}
} else {
console.log('At end of gallery: No ' + direction + ' image');
}
} else {
console.log('Error: No current image');
}
}
// Fonctions de navigation
// Swipe left -> next image
function navigateToNext() {
goto_image('next', get_next_page_url());
const newCurrentImageIdx = fullscreenContainer.data('current-image-idx') + 1;
const gallery_photos_count = {{ gallery.photo_set.count }}
if (newCurrentImageIdx > gallery_photos_count) {
return // no next image
}
// Update the current image ID and preload adjacent images
fullscreenContainer.data('current-image-idx', newCurrentImageIdx);
// swipe left
$('.image-wrapper.current').css('transform', 'translateX(-100%)');
$('.image-wrapper.next').css('transform', 'translateX(0)');
// the prev is removed and replaced by the current
$('.image-wrapper.prev').remove();
$('.image-wrapper.current').removeClass('current').addClass('prev');
// the next becomes the current
$('.image-wrapper.next').removeClass('next').addClass('current');
// the next must be created: preload the new next image and create the new next wrapper
if (newCurrentImageIdx >= gallery_photos_count) {
return // no next image
}
$.ajax({
url: getPhotoUrl(newCurrentImageIdx + 1),
method: 'GET',
success: function(response) {
const newNextWrapper = $('<div class="image-wrapper next">')
.css('transform', 'translateX(100%)')
.append(`<div class="image-container"><img src="${response.image_url}"></div>`);
newNextWrapper.find('.image-container').click(function(e) {
if (e.target === e.currentTarget) {
fullscreenContainer.fadeOut(300);
}
});
fullscreenContainer.append(newNextWrapper);
}
});
}

// Swipe right -> previous image
function navigateToPrevious() {
goto_image('prev', get_prev_page_url());
const newCurrentImageIdx = fullscreenContainer.data('current-image-idx') - 1;
if (newCurrentImageIdx < 1) {
return // no previous image
}
// Update the current image ID
fullscreenContainer.data('current-image-idx', newCurrentImageIdx);
// swipe right
$('.image-wrapper.current').css('transform', 'translateX(100%)');
$('.image-wrapper.prev').css('transform', 'translateX(0)');
// the next is removed and replaced by the current
$('.image-wrapper.next').remove();
$('.image-wrapper.current').removeClass('current').addClass('next');
// the prev becomes the current
$('.image-wrapper.prev').removeClass('prev').addClass('current');
// the prev must be created: preload the new prev image and create the new prev wrapper
if (newCurrentImageIdx <= 1) {
return // no previous image
}
$.ajax({
url: getPhotoUrl(newCurrentImageIdx - 1),
method: 'GET',
success: function(response) {
const newPrevWrapper = $('<div class="image-wrapper prev">')
.css('transform', 'translateX(-100%)')
.append(`<div class="image-container"><img src="${response.image_url}"></div>`);
newPrevWrapper.find('.image-container').click(function(e) {
if (e.target === e.currentTarget) {
fullscreenContainer.fadeOut(300);
}
});
fullscreenContainer.append(newPrevWrapper);
}
});
}

$(document).ready(function() {
const fullscreenContainer = $('#fullscreen-overlay'); // Full-screen image container
fullscreenContainer = $('#fullscreen-overlay'); // initialize Full-screen image container

// Touch event manager functions
fullscreenContainer.on('touchstart', function(e) {
touchStartX = e.originalEvent.touches[0].clientX;
// Disable transition during swipe
$('.image-wrapper').css('transition', 'none');
});

fullscreenContainer.on('touchmove', function(e) {
Expand All @@ -127,30 +207,36 @@
// Calculate displacement in real time
const currentX = e.originalEvent.touches[0].clientX;
const deltaX = currentX - touchStartX;
// Apply translation to the image during the swipe
$(this).find('img').css({
'transform': `translateX(${deltaX}px)`,
'transition': 'none'
});
// Move current image
$('.image-wrapper.current').css('transform', `translateX(${deltaX}px)`);
// Move next or previous image
if (deltaX < 0) {
// Swipe left, prepare next image
$('.image-wrapper.next')
.css('transform', `translateX(calc(100% + ${deltaX}px))`);
} else {
// Swipe right, prepare previous image
$('.image-wrapper.prev')
.css('transform', `translateX(calc(-100% + ${deltaX}px))`);
}
});

fullscreenContainer.on('touchend', function(e) {
touchEndX = e.originalEvent.changedTouches[0].clientX;
const deltaX = touchEndX - touchStartX;
// Reset image position
$(this).find('img').css({
'transform': 'translateX(0)',
'transition': 'transform 0.3s ease-out'
});
// Detect swipe direction and navigate
// Reactivate transition for end animation
$('.image-wrapper').css('transition', 'transform 0.3s ease-out');
if (Math.abs(deltaX) > minSwipeDistance) {
if (deltaX > 0) {
// Swipe right -> previous image
navigateToPrevious();
} else {
// Swipe left -> next image
navigateToNext();
}
} else {
// Return to initial position if swipe not long enough
$('.image-wrapper.current').css('transform', 'translateX(0)');
$('.image-wrapper.next').css('transform', 'translateX(100%)');
$('.image-wrapper.prev').css('transform', 'translateX(-100%)');
}
});

Expand All @@ -175,9 +261,9 @@
});

// Close full screen if clicked outside image
fullscreenContainer.click(function(e) {
if (e.target === this) {
$(this).fadeOut(300);
fullscreenContainer.find('div[class="image-container"]').click(function(e) {
if (e.target === e.currentTarget) {
fullscreenContainer.fadeOut(300);
}
});

Expand All @@ -187,7 +273,7 @@
openFullscreen($(`.gallery-image[data-fullscreen="${urlParams.get('firstImage')}"]`));
}
});
</script>
</script>
{% endblock header %}
{% block content %}
<div class="container px-3">
Expand Down
20 changes: 18 additions & 2 deletions galleries/templates/galleries/photos_gallery.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
class="gallery-image"
{%if photo.next_url%}data-next="{{photo.next_url}}"{%endif%}
data-fullscreen="{{photo.image.url}}"
{%if photo.previous_url%}data-prev="{{photo.previous_url}}"{%endif%}>
{%if photo.previous_url%}data-prev="{{photo.previous_url}}"{%endif%}
data-idx="{%get_absolute_idx_from_page page forloop.counter%}"
>
</figure>
<p>{{photo.name}}</p>
</div>
Expand All @@ -20,5 +22,19 @@
<button id="close-fullscreen">{%trans "Close"%}</button>
<button id="prev-image" class="navigation-arrow"></button>
<button id="next-image" class="navigation-arrow"></button>
<img id="fullscreen-image" src="" alt="full screen image">
<div class="image-wrapper prev" style="transform: translateX(-100%);">
<div class="image-container">
<img src="">
</div>
</div>
<div class="image-wrapper current">
<div class="image-container">
<img src="">
</div>
</div>
<div class="image-wrapper next" style="transform: translateX(100%);">
<div class="image-container">
<img src="">
</div>
</div>
</div>
1 change: 1 addition & 0 deletions galleries/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
path("<int:parent_gallery>/createsub", views_gallery.GalleryCreateView.as_view(), name="create_sub"),
path("<int:gallery>/photos", views_photo.PhotoAddView.as_view(), name="add_photo"),
path("<int:gallery>/photos/<int:photo_num>", views_photo.PhotoDetailView.as_view(), name="photo_list"),
path("<int:gallery>/photo/<int:photo_idx>", views_photo.get_photo_url, name="gallery_photo_url"),
path("photo/<int:pk>", views_photo.PhotoDetailView.as_view(), name="photo"),
path("photo/<int:pk>/edit", views_photo.PhotoEditView.as_view(), name="edit_photo"),
path("photo/<int:pk>/delete", views_photo.delete_photo, name="delete_photo"),
Expand Down
15 changes: 14 additions & 1 deletion galleries/views/views_photo.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import date
import logging
from django.forms import ValidationError
from django.http import JsonResponse
from django.shortcuts import redirect, render, get_object_or_404
from django.contrib import messages
from django.urls import reverse
Expand All @@ -9,7 +10,7 @@
from django.contrib.auth.decorators import login_required
from django.utils.translation import gettext as _
from django.core.paginator import Paginator as BasePaginator
from cousinsmatter.utils import Paginator
from cousinsmatter.utils import Paginator, assert_request_is_ajax
from ..models import Photo
from ..forms import PhotoForm

Expand Down Expand Up @@ -57,6 +58,18 @@ def get(self, request, **kwargs):
return render(request, self.template_name, {'page': page})


@login_required
def get_photo_url(request, gallery, photo_idx=1):
assert_request_is_ajax(request)
if photo_idx < 1:
return JsonResponse({'results': []})
nb_photos = Photo.objects.filter(gallery=gallery).count()
if photo_idx > nb_photos:
return JsonResponse({'results': []})
photo = Photo.objects.filter(gallery=gallery)[photo_idx-1]
return JsonResponse({'image_url': photo.image.url})


class PhotoAddView(LoginRequiredMixin, generic.CreateView):
template_name = "galleries/photo_form.html"
model = Photo
Expand Down

0 comments on commit 9be561d

Please sign in to comment.