new gallery

This commit is contained in:
j 2025-03-25 22:34:22 +00:00
parent 444a4b9306
commit 68cba1f8a4
5 changed files with 264 additions and 2 deletions

124
camp/static/css/site.scss Normal file
View file

@ -0,0 +1,124 @@
.gallery-dialog {
&:open {
background: #282828;
width: 100%;
height: 100%;
overflow: hidden;
}
&:open::backdrop {
background: black;
opacity: 0.8;
}
.photo-container {
position: relative;
max-height: 100%;
max-width: 100%;
margin: auto;
}
.photo-container .image {
max-height: 100%;
max-width: 100%;
height: calc(100vh - 180px);
}
@media screen and (max-width: 39.9375em) {
.photo-container .image {
height: auto;
width: 100%;
}
}
.main-image {
width: 100%;
height: 100%;
object-fit: contain;
}
.caption {
margin: 10px 0;
font-size: 18px;
color: lightgray;
text-align: center;
}
.thumbnails {
display: flex;
justify-content: center;
flex-wrap: wrap;
margin-top: 10px;
}
.thumbnail {
width: 80px;
height: 60px;
margin: 5px;
cursor: pointer;
border: 2px solid transparent;
}
.thumbnail.active {
border-color: blue;
}
.nav-btn {
cursor: pointer;
font-size: 20px;
padding: 10px;
background: #007BFF;
color: white;
border: none;
margin: 10px;
position: absolute;
top: 50%;
margin-top: -16px;
outline: 0;
width: 32px;
height: 32px;
text-indent: -9999px;
z-index: 9999;
}
.close {
color: lightgray !important;
cursor: pointer;
position: absolute;
top: 2px;
right: 3px;
font-size: 20px;
}
.close:hover {
color: white !important;
}
.download.disabled {
display: none;
}
.download {
color: lightgray !important;
cursor: pointer;
position: absolute;
top: 4px;
right: 24px
}
.download:hover {
color: white !important;
}
@media screen and (max-width: 39.9375em) {
.close {
bottom: 6px;
top: initial;
}
.download {
bottom: 2px;
top: initial;
}
}
.nav-btn.next {
right: 0px;
background: url('/static/css/controls.png') no-repeat -43px -32px;
}
.nav-btn.prev {
left: 0px;
background: url('/static/css/controls.png') no-repeat 0 -32px;
}
.nav-btn:disabled {
display: none;
cursor: not-allowed;
}
}

130
camp/static/js/gallery.js Normal file
View file

@ -0,0 +1,130 @@
function loadGalleries() {
document.querySelectorAll('.gallery').forEach(gallery => {
var photos = [], dialog
gallery.querySelectorAll(".photo").forEach(img => {
const src = img.src.replace('_thumbnail', '_display')
let photo = {
src: src,
caption: img.dataset.caption,
orig: img.dataset.orig
}
photos.push(photo)
img.parentElement.addEventListener("click", event => {
event.preventDefault()
event.stopPropagation()
dialog.open(src)
})
})
dialog = loadGallery(photos)
gallery.appendChild(dialog.element)
})
}
function loadGallery(images) {
var gallery = document.createElement("dialog")
gallery.classList.add("gallery-dialog")
gallery.innerHTML = `
<div class="photo-container">
<div class="image">
<img class="main-image" src="" alt="">
</div>
<button class="prev nav-btn"> Prev</button>
<button class="next nav-btn">Next </button>
</div>
<button class="close">&#10006;</button>
<a class="download disabled" target="_blank" download title="download original">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 304 384"><path fill="currentColor" d="M299 128L149 277L0 128h85V0h128v128h86zM0 320h299v43H0v-43z"/></svg>
</a>
<div class="caption"></div>
<div class="thumbnails"></div>
`
gallery.addEventListener("click", event => {
if (event.target.className == "close") {
gallery.close();
return
}
var rect = gallery.getBoundingClientRect();
var isInDialog = (rect.top <= event.clientY && event.clientY <= rect.top + rect.height &&
rect.left <= event.clientX && event.clientX <= rect.left + rect.width);
if (!isInDialog) {
gallery.close();
}
})
let currentIndex = 0;
const mainImage = gallery.querySelector(".main-image");
const caption = gallery.querySelector(".caption");
const prevBtn = gallery.querySelector(".prev.nav-btn");
const nextBtn = gallery.querySelector(".next.nav-btn");
const thumbnailsContainer = gallery.querySelector(".thumbnails");
const download = gallery.querySelector(".download");
function updateSlideshow(index) {
mainImage.src = images[index].src.replace('_display', '_thumbnail');
setTimeout(() => {
mainImage.src = images[index].src;
}, 0)
caption.innerHTML = images[index].caption;
gallery.querySelectorAll(".thumbnail").forEach((thumb, i) => {
thumb.classList.toggle("active", i === index);
});
if (images[index].orig) {
download.href = images[index].orig
download.classList.remove('disabled')
} else {
download.href = ""
download.classList.add('disabled')
}
prevBtn.disabled = index === 0;
nextBtn.disabled = index === images.length - 1;
}
function createThumbnails() {
images.forEach((img, index) => {
const thumb = document.createElement("img");
thumb.src = img.src.replace('_display', '_thumbnail');
thumb.classList.add("thumbnail");
thumb.onclick = () => {
currentIndex = index;
updateSlideshow(index);
};
thumbnailsContainer.appendChild(thumb);
});
}
function showSlideshow() {
window.dialog = gallery
gallery.showModal()
}
prevBtn.onclick = () => {
if (currentIndex > 0) {
currentIndex--;
updateSlideshow(currentIndex);
}
};
nextBtn.onclick = () => {
if (currentIndex < images.length - 1) {
currentIndex++;
updateSlideshow(currentIndex);
}
};
createThumbnails();
updateSlideshow(currentIndex);
const imagesIndex = images.map(img => img.src);
return {
element: gallery,
open: function(src) {
currentIndex = imagesIndex.indexOf(src)
updateSlideshow(currentIndex)
showSlideshow()
}
}
}
window.addEventListener("load", loadGalleries)

View file

@ -55,6 +55,7 @@
<script src="{% static "js/app.js" %}"></script> <script src="{% static "js/app.js" %}"></script>
<script src="{% static "js/what-input.js" %}"></script> <script src="{% static "js/what-input.js" %}"></script>
<script src="{% static "js/jquery.bxslider.js" %}?2"></script> <script src="{% static "js/jquery.bxslider.js" %}?2"></script>
<script src="{% static "js/gallery.js" %}"></script>
<script src="{% static "js/slideshow.js" %}"></script> <script src="{% static "js/slideshow.js" %}"></script>
{% endcompress %} {% endcompress %}
{% block end %} {% block end %}

View file

@ -1,8 +1,8 @@
{% if gallery %} {% if gallery %}
<h6><strong>{{gallery.title}}</strong></h6> <h6><strong>{{gallery.title}}</strong></h6>
<ul class="clearing-thumbs" data-clearing> <ul class="clearing-thumbs gallery" data-clearing>
{% for photo in gallery.public %} {% for photo in gallery.public %}
<li><a href="{{ gallery.get_absolute_url }}#{{forloop.counter}}"><img src="{{ photo.get_thumbnail_url }}"></a></li> <li><a href="{{ gallery.get_absolute_url }}#{{forloop.counter}}"><img src="{{ photo.get_thumbnail_url }}" class="photo" data-caption="{{ photo.caption_html }}"{% if request.user.is_staff %} data-orig="{{ photo.image.url }}"{% endif %}></a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}

View file

@ -26,6 +26,8 @@ from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from PIL import Image, ImageEnhance, ImageFile, ImageFilter from PIL import Image, ImageEnhance, ImageFile, ImageFilter
from sortedm2m.fields import SortedManyToManyField from sortedm2m.fields import SortedManyToManyField
from markdownx.utils import markdownify
from .managers import GalleryQuerySet, PhotoQuerySet from .managers import GalleryQuerySet, PhotoQuerySet
from .utils.reflection import add_reflection from .utils.reflection import add_reflection
@ -589,6 +591,11 @@ class Photo(ImageModel):
matched = True matched = True
return None return None
def caption_html(self):
caption = self.caption
if caption:
return mark_safe(markdownify(caption))
return caption
class BaseEffect(models.Model): class BaseEffect(models.Model):
name = models.CharField(_('name'), name = models.CharField(_('name'),