feat: add photo carousel for controls and improve mobile touch gestures for remark images
This commit is contained in:
parent
4e35da62b8
commit
adb5f08628
13 changed files with 201 additions and 15 deletions
Binary file not shown.
|
|
@ -1097,3 +1097,9 @@ msgstr "PDF generated and saved successfully."
|
|||
#: streetup/controls/views_reports_pdf.py:845
|
||||
msgid "Erreur lors de la génération du PDF"
|
||||
msgstr "Error generating the PDF"
|
||||
|
||||
msgid "Photos du contrôle"
|
||||
msgstr "Control photos"
|
||||
|
||||
msgid "Parcourir toutes les photos du contrôle dans le carrousel"
|
||||
msgstr "Browse all control photos in the carousel"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -144,3 +144,10 @@ msgstr ""
|
|||
#: controls/static/controls/remarks_form.js:222
|
||||
msgid "Aucune caméra disponible sur cet appareil."
|
||||
msgstr "No camera available on this device."
|
||||
|
||||
msgid "Photos du contrôle"
|
||||
msgstr "Control photos"
|
||||
|
||||
msgid "Photos de la remarque"
|
||||
msgstr "Remark photos"
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1092,3 +1092,10 @@ msgstr "PDF succesvol gegenereerd en opgeslagen."
|
|||
#: controls/views_reports_pdf.py:845
|
||||
msgid "Erreur lors de la génération du PDF"
|
||||
msgstr "Fout bij het genereren van de PDF"
|
||||
|
||||
msgid "Photos du contrôle"
|
||||
msgstr "Controlefoto's"
|
||||
|
||||
msgid "Parcourir toutes les photos du contrôle dans le carrousel"
|
||||
msgstr "Bekijk alle controlefoto's in de carrousel"
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -144,3 +144,10 @@ msgstr ""
|
|||
#: controls/static/controls/remarks_form.js:222
|
||||
msgid "Aucune caméra disponible sur cet appareil."
|
||||
msgstr "Er is geen camera beschikbaar op dit apparaat."
|
||||
|
||||
msgid "Photos du contrôle"
|
||||
msgstr "Controlefoto's"
|
||||
|
||||
msgid "Photos de la remarque"
|
||||
msgstr "Opmerking foto's"
|
||||
|
||||
|
|
|
|||
|
|
@ -49,10 +49,53 @@ body {
|
|||
background-color: #000;
|
||||
}
|
||||
|
||||
#remarkPhotoCarousel {
|
||||
touch-action: pan-y;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
#remarkPhotoCarousel .carousel-control-prev,
|
||||
#remarkPhotoCarousel .carousel-control-next {
|
||||
width: 15%;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
#remarkPhotoCarousel .carousel-control-prev-icon,
|
||||
#remarkPhotoCarousel .carousel-control-next-icon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
background-color: rgba(0, 0, 0, 0.65);
|
||||
border-radius: 50%;
|
||||
background-size: 60%;
|
||||
}
|
||||
|
||||
#remarkPhotoCarousel .carousel-indicators [data-bs-target] {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin: 0 4px;
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
#remarkPhotoCarousel .carousel-indicators .active {
|
||||
background-color: #0d6efd;
|
||||
transform: scale(1.3);
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
#remarkPhotoCarouselModal .modal-content {
|
||||
height: 100dvh;
|
||||
border-radius: 0;
|
||||
}
|
||||
#remarkPhotoCarouselModal .modal-body {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
max-height: none !important;
|
||||
}
|
||||
#remarkPhotoCarousel .carousel-inner img {
|
||||
max-height: 70dvh !important;
|
||||
}
|
||||
}
|
||||
|
||||
#remark-form-container {
|
||||
|
|
|
|||
|
|
@ -1795,15 +1795,54 @@ function bindAutocompleteForRow(card) {
|
|||
}
|
||||
})();
|
||||
|
||||
// Global variables for remark photo carousel
|
||||
// Global variables for remark / control photo carousel
|
||||
let currentRemarkCarouselPhotos = [];
|
||||
let currentRemarkCarouselIndex = 0;
|
||||
|
||||
function setCarouselModalTitle(titleText) {
|
||||
const titleEl = document.getElementById('remarkPhotoCarouselModalLabel');
|
||||
if (titleEl) {
|
||||
titleEl.textContent = titleText;
|
||||
}
|
||||
}
|
||||
|
||||
// Ouvrir toutes les photos du contrôle dans le carrousel
|
||||
function openAllControlPhotosCarousel(startIndex = 0) {
|
||||
const thumbs = Array.from(document.querySelectorAll('#remarks-list .remark-photo-thumb, #archives-list .remark-photo-thumb'));
|
||||
if (thumbs.length === 0) {
|
||||
const fallbackThumbs = Array.from(document.querySelectorAll('.remark-photo-thumb'));
|
||||
if (fallbackThumbs.length === 0) return;
|
||||
thumbs.push(...fallbackThumbs);
|
||||
}
|
||||
|
||||
currentRemarkCarouselPhotos = thumbs.map(t => ({
|
||||
fullUrl: t.dataset.fullUrl || t.src,
|
||||
thumbUrl: t.dataset.thumbUrl || t.src,
|
||||
imageId: t.dataset.imageId || null,
|
||||
caption: t.dataset.caption || '',
|
||||
remarkId: t.dataset.remarkId || null,
|
||||
element: t
|
||||
}));
|
||||
|
||||
let targetIndex = 0;
|
||||
if (typeof startIndex === 'number') {
|
||||
targetIndex = Math.max(0, Math.min(startIndex, currentRemarkCarouselPhotos.length - 1));
|
||||
} else if (startIndex instanceof HTMLElement) {
|
||||
targetIndex = thumbs.indexOf(startIndex);
|
||||
if (targetIndex < 0) targetIndex = 0;
|
||||
}
|
||||
|
||||
const controlTitle = typeof gettext !== 'undefined' ? gettext('Photos du contrôle') : 'Photos du contrôle';
|
||||
setCarouselModalTitle(controlTitle);
|
||||
showRemarkCarouselModal(targetIndex);
|
||||
}
|
||||
|
||||
// Ouvrir les photos d'une remarque spécifique dans le carrousel
|
||||
function openRemarkPhotoCarousel(thumbElement) {
|
||||
if (!thumbElement) return;
|
||||
const remarkId = thumbElement.dataset.remarkId;
|
||||
|
||||
// Find all photo thumbnails belonging to this remark
|
||||
// Trouver toutes les photos appartenant à cette remarque
|
||||
let thumbs = [];
|
||||
const remarkCard = document.getElementById(`remark-${remarkId}`);
|
||||
if (remarkCard) {
|
||||
|
|
@ -1828,6 +1867,8 @@ function openRemarkPhotoCarousel(thumbElement) {
|
|||
let startIndex = thumbs.indexOf(thumbElement);
|
||||
if (startIndex < 0) startIndex = 0;
|
||||
|
||||
const remarkTitle = typeof gettext !== 'undefined' ? gettext('Photos de la remarque') : 'Photos de la remarque';
|
||||
setCarouselModalTitle(remarkTitle);
|
||||
showRemarkCarouselModal(startIndex);
|
||||
}
|
||||
|
||||
|
|
@ -1844,6 +1885,51 @@ function openRemarkPhotoCarouselByRemarkId(remarkId) {
|
|||
}
|
||||
}
|
||||
|
||||
// Gestion des gestes tactiles (swipe mobile)
|
||||
function setupCarouselTouchGestures(carouselEl) {
|
||||
if (!carouselEl || carouselEl.dataset.touchAttached === 'true') return;
|
||||
carouselEl.dataset.touchAttached = 'true';
|
||||
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let touchEndX = 0;
|
||||
let touchEndY = 0;
|
||||
|
||||
carouselEl.addEventListener('touchstart', function(e) {
|
||||
if (e.touches.length === 1) {
|
||||
touchStartX = e.touches[0].clientX;
|
||||
touchStartY = e.touches[0].clientY;
|
||||
touchEndX = touchStartX;
|
||||
touchEndY = touchStartY;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
carouselEl.addEventListener('touchmove', function(e) {
|
||||
if (e.touches.length === 1) {
|
||||
touchEndX = e.touches[0].clientX;
|
||||
touchEndY = e.touches[0].clientY;
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
carouselEl.addEventListener('touchend', function(e) {
|
||||
const diffX = touchEndX - touchStartX;
|
||||
const diffY = touchEndY - touchStartY;
|
||||
// Détecter un balayage horizontal net (distance > 35px et plus horizontal que vertical)
|
||||
if (Math.abs(diffX) > 35 && Math.abs(diffX) > Math.abs(diffY) * 1.2) {
|
||||
if (typeof bootstrap !== 'undefined' && bootstrap.Carousel) {
|
||||
const bsCarousel = bootstrap.Carousel.getInstance(carouselEl);
|
||||
if (bsCarousel) {
|
||||
if (diffX < 0) {
|
||||
bsCarousel.next(); // Balayage vers la gauche -> Photo suivante
|
||||
} else {
|
||||
bsCarousel.prev(); // Balayage vers la droite -> Photo précédente
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { passive: true });
|
||||
}
|
||||
|
||||
function showRemarkCarouselModal(startIndex) {
|
||||
const modalEl = document.getElementById('remarkPhotoCarouselModal');
|
||||
if (!modalEl) {
|
||||
|
|
@ -1868,7 +1954,7 @@ function showRemarkCarouselModal(startIndex) {
|
|||
item.className = `carousel-item ${idx === startIndex ? 'active' : ''} h-100`;
|
||||
item.innerHTML = `
|
||||
<div class="d-flex align-items-center justify-content-center h-100 p-2">
|
||||
<img src="${photo.fullUrl}" class="d-block img-fluid mh-100 mw-100" style="object-fit: contain; max-height: 75vh;" alt="${photo.caption}">
|
||||
<img src="${photo.fullUrl}" class="d-block img-fluid mh-100 mw-100 rounded" style="object-fit: contain; max-height: 75vh;" alt="${photo.caption}">
|
||||
</div>
|
||||
`;
|
||||
carouselInner.appendChild(item);
|
||||
|
|
@ -1901,9 +1987,11 @@ function showRemarkCarouselModal(startIndex) {
|
|||
if (bsCarousel) {
|
||||
bsCarousel.dispose();
|
||||
}
|
||||
bsCarousel = new bootstrap.Carousel(carouselEl, { ride: false, interval: false });
|
||||
bsCarousel = new bootstrap.Carousel(carouselEl, { ride: false, interval: false, touch: true });
|
||||
bsCarousel.to(startIndex);
|
||||
|
||||
setupCarouselTouchGestures(carouselEl);
|
||||
|
||||
carouselEl.addEventListener('slid.bs.carousel', function (e) {
|
||||
updateRemarkCarouselActiveSlide(e.to);
|
||||
}, { once: false });
|
||||
|
|
@ -1975,6 +2063,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
bsCarousel.next();
|
||||
}
|
||||
});
|
||||
|
||||
const carouselEl = document.getElementById('remarkPhotoCarousel');
|
||||
if (carouselEl) {
|
||||
setupCarouselTouchGestures(carouselEl);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -91,6 +91,16 @@
|
|||
</p>
|
||||
<p class="mb-1"><strong>{% translate 'Lieu' %} :</strong> {{ control.location }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if control_photos_count > 0 %}
|
||||
<div class="mt-3">
|
||||
<button type="button" class="btn btn-outline-primary d-inline-flex align-items-center gap-2 py-2 px-3 shadow-sm rounded-3" onclick="openAllControlPhotosCarousel(0)" title="{% translate 'Parcourir toutes les photos du contrôle dans le carrousel' %}">
|
||||
<i class="bi bi-images fs-5"></i>
|
||||
<span class="fw-semibold">{% translate "Photos du contrôle" %}</span>
|
||||
<span class="badge bg-primary rounded-pill">{{ control_photos_count }}</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
|
||||
|
|
@ -776,20 +786,20 @@
|
|||
|
||||
</div>
|
||||
|
||||
<!-- Modal Carrousel pour les photos d'une remarque -->
|
||||
<!-- Modal Carrousel pour les photos d'un contrôle ou d'une remarque -->
|
||||
<div class="modal fade" id="remarkPhotoCarouselModal" tabindex="-1" aria-labelledby="remarkPhotoCarouselModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-dialog modal-fullscreen-sm-down modal-xl modal-dialog-centered">
|
||||
<div class="modal-content bg-dark text-white shadow-lg border-0">
|
||||
<div class="modal-header border-bottom border-secondary py-2 px-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-images text-primary fs-5"></i>
|
||||
<h5 class="modal-title fs-6 mb-0 text-white" id="remarkPhotoCarouselModalLabel">{% translate "Photos de la remarque" %}</h5>
|
||||
<h5 class="modal-title fs-6 mb-0 text-white" id="remarkPhotoCarouselModalLabel">{% translate "Photos du contrôle" %}</h5>
|
||||
<span class="badge bg-primary rounded-pill px-2 py-1 fs-7" id="remark-carousel-counter">1 / 1</span>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0 position-relative bg-black d-flex align-items-center justify-content-center overflow-hidden" style="min-height: 350px; max-height: 80vh;">
|
||||
<div id="remarkPhotoCarousel" class="carousel slide w-100 h-100 d-flex flex-column justify-content-center" data-bs-ride="false">
|
||||
<div id="remarkPhotoCarousel" class="carousel slide w-100 h-100 d-flex flex-column justify-content-center" data-bs-ride="false" data-bs-touch="true">
|
||||
<div class="carousel-indicators mb-2" id="remark-carousel-indicators"></div>
|
||||
<div class="carousel-inner h-100" id="remark-carousel-inner"></div>
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#remarkPhotoCarousel" data-bs-slide="prev" style="z-index: 10;">
|
||||
|
|
|
|||
|
|
@ -127,6 +127,16 @@
|
|||
{{ control.description }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if control_photos_count > 0 %}
|
||||
<div class="mt-3">
|
||||
<button type="button" class="btn btn-outline-primary w-100 d-flex align-items-center justify-content-center gap-2 py-2 shadow-sm rounded-3" onclick="openAllControlPhotosCarousel(0)" title="{% translate 'Parcourir toutes les photos du contrôle dans le carrousel' %}">
|
||||
<i class="bi bi-images fs-5"></i>
|
||||
<span class="fw-semibold">{% translate "Photos du contrôle" %}</span>
|
||||
<span class="badge bg-primary rounded-pill">{{ control_photos_count }}</span>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -1151,20 +1161,22 @@ document.getElementById('force-sync-btn')?.addEventListener('click', async () =>
|
|||
await displayPendingRemarks();
|
||||
}
|
||||
});
|
||||
<!-- Modal Carrousel pour les photos d'une remarque -->
|
||||
</script>
|
||||
|
||||
<!-- Modal Carrousel pour les photos d'un contrôle ou d'une remarque -->
|
||||
<div class="modal fade" id="remarkPhotoCarouselModal" tabindex="-1" aria-labelledby="remarkPhotoCarouselModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||||
<div class="modal-dialog modal-fullscreen-sm-down modal-xl modal-dialog-centered">
|
||||
<div class="modal-content bg-dark text-white shadow-lg border-0">
|
||||
<div class="modal-header border-bottom border-secondary py-2 px-3">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i class="bi bi-images text-primary fs-5"></i>
|
||||
<h5 class="modal-title fs-6 mb-0 text-white" id="remarkPhotoCarouselModalLabel">{% translate "Photos de la remarque" %}</h5>
|
||||
<h5 class="modal-title fs-6 mb-0 text-white" id="remarkPhotoCarouselModalLabel">{% translate "Photos du contrôle" %}</h5>
|
||||
<span class="badge bg-primary rounded-pill px-2 py-1 fs-7" id="remark-carousel-counter">1 / 1</span>
|
||||
</div>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0 position-relative bg-black d-flex align-items-center justify-content-center overflow-hidden" style="min-height: 300px; max-height: 80vh;">
|
||||
<div id="remarkPhotoCarousel" class="carousel slide w-100 h-100 d-flex flex-column justify-content-center" data-bs-ride="false">
|
||||
<div id="remarkPhotoCarousel" class="carousel slide w-100 h-100 d-flex flex-column justify-content-center" data-bs-ride="false" data-bs-touch="true">
|
||||
<div class="carousel-indicators mb-2" id="remark-carousel-indicators"></div>
|
||||
<div class="carousel-inner h-100" id="remark-carousel-inner"></div>
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#remarkPhotoCarousel" data-bs-slide="prev" style="z-index: 10;">
|
||||
|
|
@ -1194,7 +1206,6 @@ document.getElementById('force-sync-btn')?.addEventListener('click', async () =>
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
|
|
|
|||
|
|
@ -729,8 +729,9 @@ def controls_detail(request, control_id):
|
|||
|
||||
|
||||
|
||||
remarks = control.remarks.exclude(archived=True)
|
||||
archives = control.remarks.filter(archived=True)
|
||||
remarks = control.remarks.exclude(archived=True).prefetch_related('images', 'history__images', 'remark_tags__category', 'quantities')
|
||||
archives = control.remarks.filter(archived=True).prefetch_related('images', 'remark_tags__category')
|
||||
control_photos_count = RemarkImage.objects.filter(remark__in=remarks).count()
|
||||
plans = control.reference_plans.all()
|
||||
reports = control.reports.all()
|
||||
categories = Category.objects.all()
|
||||
|
|
@ -783,6 +784,7 @@ def controls_detail(request, control_id):
|
|||
"control": control,
|
||||
"remarks": remarks,
|
||||
"archives": archives,
|
||||
"control_photos_count": control_photos_count,
|
||||
"plans": plans,
|
||||
"reports": reports,
|
||||
"categories": categories,
|
||||
|
|
|
|||
Loading…
Reference in a new issue