From ddbd3a685939c313867a6144b6cc3608832b4f47 Mon Sep 17 00:00:00 2001 From: Junwei Zhao Date: Sun, 24 Nov 2024 09:30:04 +1100 Subject: [PATCH] Add desctiption to image --- data/db.sqlite3 | Bin 262144 -> 262144 bytes links/api_views.py | 18 +- links/serializers.py | 12 +- links/templates/links/collection_detail.html | 249 ++++++++++++++++-- .../templates/links/collection_slideshow.html | 48 +++- new_theme/static/css/dist/styles.css | 4 + 6 files changed, 297 insertions(+), 34 deletions(-) diff --git a/data/db.sqlite3 b/data/db.sqlite3 index 4dbd7203d00b930582123521f5596bb036833475..01ad6b39472916e83776213e0139d68e94f6c086 100644 GIT binary patch delta 462 zcmZo@5NK!+m>|ulF;T{uQKK=THGy$!0@Ff&Vg5S|7VJkDI3hTVIr!LLvLE5g+Ab2n z)XmPqm&KquIZ;4y`uuPvmF+LWnVQ%@OwH*kaZC#Bo8y?aZ;oU3n<&eg#=y;3&%k$$ zcOB0H?mn(MP7C%=Y_?1n80%T2Hzx`tGO{=M*>d|<3s0B-!W_q3Ql3A(_6zfNAtPf2 zBO@y#6DvbAJyT0VGc$|n5AHK5us3ZxRRHQsQOHaK>P=Nh&d)0;%Fj_RR!FT#OfFF|ulJWz|mTAVwyAW^RWWX=G($p=V)eYGiBzRc#Gc?fMO-+VVSd X`}AesnfaMb4UEBRwtxQ4%*z1)2~S}{ diff --git a/links/api_views.py b/links/api_views.py index 5e60446..edb125f 100644 --- a/links/api_views.py +++ b/links/api_views.py @@ -1,9 +1,8 @@ from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response -from django.shortcuts import get_object_or_404 from .models import ImageCollection, Image -from .serializers import ImageCollectionSerializer, ImageSerializer +from .serializers import ImageCollectionSerializer, ImageSerializer, ImageDescriptionSerializer from .storage import R2Storage import uuid import logging @@ -132,6 +131,21 @@ class ImageViewSet(viewsets.ModelViewSet): queryset = Image.objects.all() serializer_class = ImageSerializer + @action(detail=True, methods=['patch'], url_path='update-description') + def update_description(self, request, pk=None): + """Update the description of an image""" + image = self.get_object() + serializer = ImageDescriptionSerializer(image, data=request.data, partial=True) + + if serializer.is_valid(): + serializer.save() + return Response({ + 'status': 'success', + 'message': 'Description updated successfully', + 'data': serializer.data + }) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def perform_destroy(self, instance): """Delete image from storage when deleting record""" storage = R2Storage() diff --git a/links/serializers.py b/links/serializers.py index 2fd1767..75d020e 100644 --- a/links/serializers.py +++ b/links/serializers.py @@ -25,12 +25,16 @@ class ImageSerializer(serializers.ModelSerializer): class Meta: model = Image - fields = ['id', 'title', 'description', 'url', 'content_type', 'size', 'created_at'] + fields = ['id', 'collection', 'title', 'description', 'content_type', 'size', 'created_at', 'updated_at', 'url'] + read_only_fields = ['id', 'collection', 'content_type', 'size', 'created_at', 'updated_at'] def get_url(self, obj): - from .storage import R2Storage - storage = R2Storage() - return storage.get_url(obj.file_key) + return obj.get_url() + +class ImageDescriptionSerializer(serializers.ModelSerializer): + class Meta: + model = Image + fields = ['description'] class ImageCollectionSerializer(serializers.ModelSerializer): image_count = serializers.SerializerMethodField() diff --git a/links/templates/links/collection_detail.html b/links/templates/links/collection_detail.html index abe9548..a94a790 100644 --- a/links/templates/links/collection_detail.html +++ b/links/templates/links/collection_detail.html @@ -16,6 +16,144 @@ .dropzone .dz-message { margin: 2em 0; } + .image-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; + padding: 1rem; + } + .image-item { + position: relative; + aspect-ratio: 1; + overflow: hidden; + border-radius: 0.5rem; + background: #f3f4f6; + } + .image-item img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; + } + .image-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + opacity: 0; + transition: opacity 0.3s ease; + } + .image-item:hover .image-overlay { + opacity: 1; + } + .image-item:hover img { + transform: scale(1.05); + } + .image-action { + background: rgba(255, 255, 255, 0.2); + border: none; + color: white; + width: 2.5rem; + height: 2.5rem; + border-radius: 9999px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.2s; + } + .image-action:hover { + background: rgba(255, 255, 255, 0.3); + } + .image-action svg { + width: 1.25rem; + height: 1.25rem; + } + .description-modal { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.75); + z-index: 50; + align-items: center; + justify-content: center; + padding: 1rem; + } + .modal-content { + background: white; + padding: 1.5rem; + border-radius: 0.5rem; + width: 100%; + max-width: 500px; + position: relative; + } + .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + } + .modal-title { + font-size: 1.25rem; + font-weight: 600; + color: #1f2937; + } + .modal-close { + background: none; + border: none; + color: #6b7280; + cursor: pointer; + padding: 0.5rem; + } + .modal-close:hover { + color: #1f2937; + } + .modal-body textarea { + width: 100%; + min-height: 120px; + padding: 0.75rem; + border: 1px solid #e5e7eb; + border-radius: 0.375rem; + margin-bottom: 1rem; + resize: vertical; + } + .modal-footer { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + } + .modal-button { + padding: 0.5rem 1rem; + border-radius: 0.375rem; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s; + } + .cancel-button { + background: #f3f4f6; + border: 1px solid #e5e7eb; + color: #374151; + } + .cancel-button:hover { + background: #e5e7eb; + } + .save-button { + background: #2563eb; + border: 1px solid #2563eb; + color: white; + } + .save-button:hover { + background: #1d4ed8; + } {% endblock %} @@ -90,14 +228,10 @@
{% for image in collection.images.all %} -
-
- {{ image.title }} - -
- + {{ image.title }} + + + +
{% empty %} @@ -158,6 +293,27 @@
+ +
+ +
+ {% endblock %} {% block extra_js %} @@ -180,7 +336,7 @@ const dropzone = new Dropzone("#uploadForm", {
+ d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>

{% trans "Drag and drop images here, or click to select files" %} @@ -268,5 +424,60 @@ function deleteCollection(collectionId) { }); } } + +let currentImageId = null; + +function editDescription(imageId, description) { + currentImageId = imageId; + const modal = document.getElementById('descriptionModal'); + const textarea = document.getElementById('imageDescription'); + textarea.value = description; + modal.style.display = 'flex'; +} + +function closeDescriptionModal() { + const modal = document.getElementById('descriptionModal'); + modal.style.display = 'none'; + currentImageId = null; +} + +async function saveImageDescription() { + if (!currentImageId) return; + + const description = document.getElementById('imageDescription').value; + + try { + const response = await fetch(`/api/images/${currentImageId}/update-description`, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value + }, + body: JSON.stringify({ description: description }) + }); + + if (response.ok) { + const data = await response.json(); + closeDescriptionModal(); + } else { + alert('Failed to update description. Please try again.'); + } + } catch (error) { + console.error('Error updating description:', error); + alert('Failed to update description. Please try again.'); + } +} + +// Close modal when clicking outside +document.getElementById('descriptionModal').addEventListener('click', function(e) { + if (e.target === this) { + closeDescriptionModal(); + } +}); + +// Prevent modal close when clicking modal content +document.querySelector('.modal-content').addEventListener('click', function(e) { + e.stopPropagation(); +}); {% endblock %} diff --git a/links/templates/links/collection_slideshow.html b/links/templates/links/collection_slideshow.html index 31b1c8f..8aa74da 100644 --- a/links/templates/links/collection_slideshow.html +++ b/links/templates/links/collection_slideshow.html @@ -58,6 +58,21 @@ max-height: 100%; object-fit: contain; } + .image-description { + position: absolute; + bottom: 5rem; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, 0.7); + padding: 1rem; + border-radius: 0.5rem; + color: white; + max-width: 80%; + text-align: center; + } + .image-description p { + margin: 0; + } .controls { position: fixed; bottom: 0; @@ -356,8 +371,13 @@

{% for image in collection.images.all %} -
+
{{ image.title }} + {% if image.description %} +
+

{{ image.description }}

+
+ {% endif %}
{% endfor %} @@ -443,6 +463,13 @@
+
+

{% trans "Description Display" %}

+
+ + +
+
@@ -701,6 +728,9 @@ document.addEventListener('DOMContentLoaded', async () => { startSlideshow(); // Initialize interval value display document.getElementById('intervalValue').textContent = `${INTERVAL_TIME/1000}s`; + // Enable descriptions by default + document.getElementById('showDescriptions').checked = true; + toggleDescriptions(true); // Add transition effect listeners document.querySelectorAll('input[name="transition-effect"]').forEach(radio => { @@ -730,7 +760,7 @@ function togglePlayPause() { if (isMusicPlaying) backgroundMusic.play(); } else { stopSlideshow(); - icon.innerHTML = ''; + icon.innerHTML = ''; text.textContent = '{% trans "Play" %}'; backgroundMusic.pause(); } @@ -830,12 +860,12 @@ document.addEventListener('keydown', (e) => { } }); -// Start slideshow when page loads -document.addEventListener('DOMContentLoaded', async () => { - await fetchPlaylist(); - startSlideshow(); - // Initialize interval value display - document.getElementById('intervalValue').textContent = `${INTERVAL_TIME/1000}s`; -}); +function toggleDescriptions(show) { + document.querySelectorAll('.image-description').forEach(desc => { + if (desc.querySelector('.description-text').textContent.trim()) { + desc.style.display = show ? 'block' : 'none'; + } + }); +} {% endblock %} diff --git a/new_theme/static/css/dist/styles.css b/new_theme/static/css/dist/styles.css index 8766507..0513467 100644 --- a/new_theme/static/css/dist/styles.css +++ b/new_theme/static/css/dist/styles.css @@ -994,6 +994,10 @@ video { user-select: all; } +.resize { + resize: both; +} + .list-inside { list-style-position: inside; }