diff --git a/app/static/js/modal-pagination.js b/app/static/js/modal-pagination.js index 6bce90a..e5dc2fa 100644 --- a/app/static/js/modal-pagination.js +++ b/app/static/js/modal-pagination.js @@ -1,14 +1,12 @@ // /app/static/js/modal-pagination.js + document.addEventListener('DOMContentLoaded', () => { const modal = document.getElementById('imageModal'); - const modalImage = document.getElementById('modalImage'); const modalPrev = document.getElementById('modalPrev'); const modalNext = document.getElementById('modalNext'); const modalClose = document.getElementById('modalClose'); const modalImageWrapper = document.querySelector('.modal-image-wrapper'); - modalImage.setAttribute('draggable', 'false'); - let images = Array.from(document.querySelectorAll('.img-clickable')); let currentIndex = -1; let isZoomed = false; @@ -25,7 +23,26 @@ document.addEventListener('DOMContentLoaded', () => { function updateImage(index) { const img = images[index]; - modalImage.src = img.dataset.full; + const fileType = img.dataset.type || "image"; + modalImageWrapper.innerHTML = ""; + + if (fileType === "video") { + const video = document.createElement("video"); + video.src = img.dataset.full; + video.controls = true; + video.autoplay = true; + video.style.maxWidth = "100%"; + video.style.maxHeight = "100%"; + modalImageWrapper.appendChild(video); + } else { + const image = document.createElement("img"); + image.src = img.dataset.full; + image.alt = "Full View"; + image.setAttribute("draggable", "false"); + image.id = "modalImage"; + modalImageWrapper.appendChild(image); + } + currentIndex = index; resetZoom(); } @@ -38,7 +55,7 @@ document.addEventListener('DOMContentLoaded', () => { function closeModal() { modal.classList.remove('active'); - modalImage.src = ''; + modalImageWrapper.innerHTML = ''; currentIndex = -1; resetZoom(); history.replaceState({}, '', window.location.pathname + window.location.search); @@ -65,15 +82,10 @@ document.addEventListener('DOMContentLoaded', () => { img.addEventListener('click', () => openModal(i)); }); - const newHasNext = doc.getElementById('hasNextPage')?.value || "false"; - const newNextUrl = doc.getElementById('nextPageUrl')?.value || ""; - const newHasPrev = doc.getElementById('hasPrevPage')?.value || "false"; - const newPrevUrl = doc.getElementById('prevPageUrl')?.value || ""; - - hasNextPageInput.value = newHasNext; - nextPageUrlInput.value = newNextUrl; - hasPrevPageInput.value = newHasPrev; - prevPageUrlInput.value = newPrevUrl; + hasNextPageInput.value = doc.getElementById('hasNextPage')?.value || "false"; + nextPageUrlInput.value = doc.getElementById('nextPageUrl')?.value || ""; + hasPrevPageInput.value = doc.getElementById('hasPrevPage')?.value || "false"; + prevPageUrlInput.value = doc.getElementById('prevPageUrl')?.value || ""; history.pushState({}, '', nextPageUrlInput.value); openModal(0); @@ -103,15 +115,10 @@ document.addEventListener('DOMContentLoaded', () => { img.addEventListener('click', () => openModal(i)); }); - const newHasNext = doc.getElementById('hasNextPage')?.value || "false"; - const newNextUrl = doc.getElementById('nextPageUrl')?.value || ""; - const newHasPrev = doc.getElementById('hasPrevPage')?.value || "false"; - const newPrevUrl = doc.getElementById('prevPageUrl')?.value || ""; - - hasNextPageInput.value = newHasNext; - nextPageUrlInput.value = newNextUrl; - hasPrevPageInput.value = newHasPrev; - prevPageUrlInput.value = newPrevUrl; + hasNextPageInput.value = doc.getElementById('hasNextPage')?.value || "false"; + nextPageUrlInput.value = doc.getElementById('nextPageUrl')?.value || ""; + hasPrevPageInput.value = doc.getElementById('hasPrevPage')?.value || "false"; + prevPageUrlInput.value = doc.getElementById('prevPageUrl')?.value || ""; history.pushState({}, '', prevPageUrlInput.value); openModal(images.length - 1); @@ -121,6 +128,8 @@ document.addEventListener('DOMContentLoaded', () => { } function toggleZoom() { + const isImage = modalImageWrapper.querySelector('img') !== null; + if (!isImage) return; isZoomed = !isZoomed; modalImageWrapper.classList.toggle('zoomed', isZoomed); modalImageWrapper.style.cursor = isZoomed ? 'grab' : ''; @@ -139,7 +148,8 @@ document.addEventListener('DOMContentLoaded', () => { } modalImageWrapper.addEventListener('mousedown', (e) => { - if (!isZoomed) return; + const isImage = modalImageWrapper.querySelector('img') !== null; + if (!isZoomed || !isImage) return; isDragging = true; dragMoved = false; modalImageWrapper.style.cursor = 'grabbing'; @@ -153,16 +163,19 @@ document.addEventListener('DOMContentLoaded', () => { modalImageWrapper.addEventListener('mouseleave', () => { isDragging = false; - if (isZoomed) modalImageWrapper.style.cursor = 'grab'; + const isImage = modalImageWrapper.querySelector('img') !== null; + if (isZoomed && isImage) modalImageWrapper.style.cursor = 'grab'; }); modalImageWrapper.addEventListener('mouseup', () => { isDragging = false; - if (isZoomed) modalImageWrapper.style.cursor = 'grab'; + const isImage = modalImageWrapper.querySelector('img') !== null; + if (isZoomed && isImage) modalImageWrapper.style.cursor = 'grab'; }); modalImageWrapper.addEventListener('mousemove', (e) => { - if (!isDragging || !isZoomed) return; + const isImage = modalImageWrapper.querySelector('img') !== null; + if (!isDragging || !isZoomed || !isImage) return; e.preventDefault(); dragMoved = true; const x = e.pageX - modalImageWrapper.offsetLeft; diff --git a/app/static/style.css b/app/static/style.css index e1f5cdb..4b179f6 100644 --- a/app/static/style.css +++ b/app/static/style.css @@ -114,6 +114,7 @@ body { } .gallery-thumb { + position: relative; width: 100%; height: 100%; background-size: cover; @@ -135,6 +136,18 @@ body { color: #d4d4d4; } +.play-overlay { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 3rem; + color: white; + text-shadow: 0 0 8px rgba(0, 0, 0, 0.8); + opacity: 1; + pointer-events: none; + z-index: 2; +} .pagination-container { text-align: center; diff --git a/app/templates/_gallery_grid.html b/app/templates/_gallery_grid.html index e3de371..56c29c6 100644 --- a/app/templates/_gallery_grid.html +++ b/app/templates/_gallery_grid.html @@ -3,10 +3,15 @@ {% for image in images.items %} diff --git a/app/utils/image_importer.py b/app/utils/image_importer.py index 9fa757e..dc08cb1 100644 --- a/app/utils/image_importer.py +++ b/app/utils/image_importer.py @@ -1,5 +1,4 @@ # app/utils/image_importer.py - from datetime import datetime import os, shutil, hashlib, time from PIL import Image @@ -7,6 +6,7 @@ import exifread import mimetypes import imagehash import uuid +import subprocess from app import db from app.models import ImageRecord @@ -29,7 +29,7 @@ def import_images_task(source_dir, dest_dir): for root, _, files in os.walk(artist_path): for file in files: - if file.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff')): + if file.lower().endswith(('.jpg', '.jpeg', '.jfif', '.png', '.gif', '.bmp', '.tiff', '.webp', '.mp4', '.mov')): full_path = os.path.join(root, file) filename = os.path.basename(full_path) @@ -61,7 +61,12 @@ def import_images_task(source_dir, dest_dir): shutil.copy2(full_path, dest_path) try: - thumb_path = generate_thumbnail(dest_path) + if dest_path.lower().endswith(('.mp4', '.mov')): + thumb_filename = f"{uuid.uuid4().hex}.jpg" + thumb_path = os.path.join("/images/thumbs", thumb_filename) + thumb_path = generate_video_thumbnail(dest_path, thumb_path) + else: + thumb_path = generate_thumbnail(dest_path) except Exception as e: print(f"[WARN] Failed to generate thumbnail for {filename}: {e}") thumb_path = None @@ -128,6 +133,24 @@ def generate_thumbnail(image_path, size=(400, 400), overwrite=False): return str(thumb_path) +def generate_video_thumbnail(video_path, output_path, time_position="00:00:01"): + os.makedirs(os.path.dirname(output_path), exist_ok=True) + + command = [ + "ffmpeg", + "-i", video_path, + "-ss", time_position, + "-vframes", "1", + "-vf", f"scale={THUMB_SIZE[0]}:-1", + output_path + ] + + try: + subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + return output_path + except subprocess.CalledProcessError as e: + print(f"[WARN] Failed to extract video thumbnail for {video_path}: {e}") + return None def calculate_hash(file_path): hash_func = hashlib.sha256() @@ -165,6 +188,29 @@ def extract_metadata(file_path): 'taken_at': None } + metadata['file_size'] = os.path.getsize(file_path) + ext = os.path.splitext(file_path)[1].lower() + + if ext in ('.mp4', '.mov'): + # Handle video dimensions using ffprobe + try: + import subprocess, json + cmd = [ + "ffprobe", "-v", "error", + "-select_streams", "v:0", + "-show_entries", "stream=width,height", + "-of", "json", file_path + ] + result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True) + data = json.loads(result.stdout) + stream = data.get("streams", [{}])[0] + metadata['width'] = stream.get('width') + metadata['height'] = stream.get('height') + metadata['format'] = 'mp4' + except Exception as e: + print(f"[WARN] Failed to extract video metadata: {file_path} – {e}") + return metadata + try: metadata['file_size'] = os.path.getsize(file_path) with Image.open(file_path) as img: diff --git a/dockerfile b/dockerfile index 4259383..9795a6d 100644 --- a/dockerfile +++ b/dockerfile @@ -6,7 +6,7 @@ WORKDIR /app # Install PostgreSQL client RUN apt-get update && \ - apt-get install -y postgresql-client && \ + apt-get install -y postgresql-client ffmpeg && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*