321 lines
10 KiB
JavaScript
321 lines
10 KiB
JavaScript
// /app/static/js/modal-pagination.js
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const modal = document.getElementById('imageModal');
|
||
const modalPrev = document.getElementById('modalPrev');
|
||
const modalNext = document.getElementById('modalNext');
|
||
const modalClose = document.getElementById('modalClose');
|
||
const modalImageWrapper = document.querySelector('.modal-image-wrapper');
|
||
|
||
// NEW: tag editor elements
|
||
const tagEditor = document.getElementById('modalTagEditor');
|
||
const tagList = document.getElementById('modalTagList');
|
||
const tagForm = document.getElementById('modalTagForm');
|
||
const tagInput = tagForm ? tagForm.querySelector('input[name="name"]') : null;
|
||
|
||
let images = Array.from(document.querySelectorAll('.img-clickable'));
|
||
let currentIndex = -1;
|
||
let isZoomed = false;
|
||
let isDragging = false;
|
||
let startX, startY, scrollLeft, scrollTop;
|
||
let dragStartX = 0;
|
||
let dragStartY = 0;
|
||
let dragMoved = false;
|
||
|
||
const hasNextPageInput = document.getElementById('hasNextPage');
|
||
const nextPageUrlInput = document.getElementById('nextPageUrl');
|
||
const hasPrevPageInput = document.getElementById('hasPrevPage');
|
||
const prevPageUrlInput = document.getElementById('prevPageUrl');
|
||
|
||
// ---------------------------
|
||
// Tag editor helpers
|
||
// ---------------------------
|
||
function setEditorImageId(id) {
|
||
if (tagEditor) tagEditor.dataset.imageId = id || '';
|
||
}
|
||
function getEditorImageId() {
|
||
return tagEditor ? tagEditor.dataset.imageId : '';
|
||
}
|
||
function renderTags(tags) {
|
||
if (!tagList) return;
|
||
tagList.innerHTML = '';
|
||
(tags || []).forEach(t => {
|
||
const chip = document.createElement('span');
|
||
chip.className = 'tag-chip';
|
||
const label = (t.kind === 'artist')
|
||
? `🎨 ${t.name.split(':', 1)[0] === 'artist' ? t.name.split(':', 2)[1] : t.name}`
|
||
: (t.kind === 'archive')
|
||
? `🗜️ ${t.name.split(':', 1)[0] === 'archive' ? t.name.split(':', 2)[1] : t.name}`
|
||
: `#${t.name}`;
|
||
chip.innerHTML = `${label} <button class="x" data-name="${t.name}" title="Remove">×</button>`;
|
||
tagList.appendChild(chip);
|
||
});
|
||
}
|
||
async function loadTags(imageId) {
|
||
if (!imageId) { renderTags([]); return; }
|
||
try {
|
||
const r = await fetch(`/image/${imageId}/tags`);
|
||
const j = await r.json();
|
||
if (j.ok) renderTags(j.tags);
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
async function addTag(imageId, name) {
|
||
const fd = new FormData();
|
||
fd.append('name', name);
|
||
const r = await fetch(`/image/${imageId}/tags/add`, { method: 'POST', body: fd });
|
||
const j = await r.json();
|
||
if (j.ok) {
|
||
await loadTags(imageId);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
async function removeTag(imageId, name) {
|
||
const fd = new FormData();
|
||
fd.append('name', name);
|
||
const r = await fetch(`/image/${imageId}/tags/remove`, { method: 'POST', body: fd });
|
||
const j = await r.json();
|
||
if (j.ok) {
|
||
await loadTags(imageId);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
if (tagForm) {
|
||
tagForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const id = getEditorImageId();
|
||
const name = (tagInput.value || '').trim();
|
||
if (!id || !name) return;
|
||
await addTag(id, name);
|
||
tagInput.value = '';
|
||
});
|
||
}
|
||
if (tagList) {
|
||
tagList.addEventListener('click', async (e) => {
|
||
const btn = e.target.closest('button.x');
|
||
if (!btn) return;
|
||
const id = getEditorImageId();
|
||
const name = btn.dataset.name;
|
||
if (!id || !name) return;
|
||
await removeTag(id, name);
|
||
});
|
||
}
|
||
|
||
// ---------------------------
|
||
// Existing viewer logic
|
||
// ---------------------------
|
||
function updateImage(index) {
|
||
const img = images[index];
|
||
const fileType = img.dataset.type || "image";
|
||
const imageId = img.dataset.id || "";
|
||
|
||
// clear wrapper
|
||
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();
|
||
|
||
// NEW: load tags for this image into the modal editor
|
||
setEditorImageId(imageId);
|
||
loadTags(imageId);
|
||
}
|
||
|
||
function openModal(index) {
|
||
modal.classList.add('active');
|
||
updateImage(index);
|
||
history.replaceState({ modalIndex: index }, '', window.location.href);
|
||
}
|
||
|
||
function closeModal() {
|
||
modal.classList.remove('active');
|
||
modalImageWrapper.innerHTML = '';
|
||
currentIndex = -1;
|
||
resetZoom();
|
||
// clear tag UI
|
||
setEditorImageId('');
|
||
renderTags([]);
|
||
history.replaceState({}, '', window.location.pathname + window.location.search);
|
||
}
|
||
|
||
function showNext() {
|
||
if (currentIndex < images.length - 1) {
|
||
updateImage(currentIndex + 1);
|
||
} else if (hasNextPageInput?.value === "true") {
|
||
fetch(nextPageUrlInput.value)
|
||
.then(res => res.text())
|
||
.then(html => {
|
||
const parser = new DOMParser();
|
||
const doc = parser.parseFromString(html, 'text/html');
|
||
const newGallery = doc.querySelector('.gallery-grid');
|
||
const newImages = doc.querySelectorAll('.img-clickable');
|
||
|
||
if (newGallery && newImages.length > 0) {
|
||
const gallery = document.querySelector('.gallery-grid');
|
||
gallery.innerHTML = newGallery.innerHTML;
|
||
|
||
images = Array.from(document.querySelectorAll('.img-clickable'));
|
||
images.forEach((img, i) => {
|
||
img.addEventListener('click', () => openModal(i));
|
||
});
|
||
|
||
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);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function showPrev() {
|
||
if (currentIndex > 0) {
|
||
updateImage(currentIndex - 1);
|
||
} else if (hasPrevPageInput?.value === "true") {
|
||
fetch(prevPageUrlInput.value)
|
||
.then(res => res.text())
|
||
.then(html => {
|
||
const parser = new DOMParser();
|
||
const doc = parser.parseFromString(html, 'text/html');
|
||
const newGallery = doc.querySelector('.gallery-grid');
|
||
const newImages = doc.querySelectorAll('.img-clickable');
|
||
|
||
if (newGallery && newImages.length > 0) {
|
||
const gallery = document.querySelector('.gallery-grid');
|
||
gallery.innerHTML = newGallery.innerHTML;
|
||
|
||
images = Array.from(document.querySelectorAll('.img-clickable'));
|
||
images.forEach((img, i) => {
|
||
img.addEventListener('click', () => openModal(i));
|
||
});
|
||
|
||
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);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function toggleZoom() {
|
||
const isImage = modalImageWrapper.querySelector('img') !== null;
|
||
if (!isImage) return;
|
||
isZoomed = !isZoomed;
|
||
modalImageWrapper.classList.toggle('zoomed', isZoomed);
|
||
modalImageWrapper.style.cursor = isZoomed ? 'grab' : '';
|
||
if (!isZoomed) {
|
||
modalImageWrapper.scrollLeft = 0;
|
||
modalImageWrapper.scrollTop = 0;
|
||
}
|
||
}
|
||
|
||
function resetZoom() {
|
||
isZoomed = false;
|
||
modalImageWrapper.classList.remove('zoomed');
|
||
modalImageWrapper.scrollLeft = 0;
|
||
modalImageWrapper.scrollTop = 0;
|
||
modalImageWrapper.style.cursor = '';
|
||
}
|
||
|
||
modalImageWrapper.addEventListener('mousedown', (e) => {
|
||
const isImage = modalImageWrapper.querySelector('img') !== null;
|
||
if (!isZoomed || !isImage) return;
|
||
isDragging = true;
|
||
dragMoved = false;
|
||
modalImageWrapper.style.cursor = 'grabbing';
|
||
startX = e.pageX - modalImageWrapper.offsetLeft;
|
||
startY = e.pageY - modalImageWrapper.offsetTop;
|
||
scrollLeft = modalImageWrapper.scrollLeft;
|
||
scrollTop = modalImageWrapper.scrollTop;
|
||
dragStartX = e.pageX;
|
||
dragStartY = e.pageY;
|
||
});
|
||
|
||
modalImageWrapper.addEventListener('mouseleave', () => {
|
||
isDragging = false;
|
||
const isImage = modalImageWrapper.querySelector('img') !== null;
|
||
if (isZoomed && isImage) modalImageWrapper.style.cursor = 'grab';
|
||
});
|
||
|
||
modalImageWrapper.addEventListener('mouseup', () => {
|
||
isDragging = false;
|
||
const isImage = modalImageWrapper.querySelector('img') !== null;
|
||
if (isZoomed && isImage) modalImageWrapper.style.cursor = 'grab';
|
||
});
|
||
|
||
modalImageWrapper.addEventListener('mousemove', (e) => {
|
||
const isImage = modalImageWrapper.querySelector('img') !== null;
|
||
if (!isDragging || !isZoomed || !isImage) return;
|
||
e.preventDefault();
|
||
dragMoved = true;
|
||
const x = e.pageX - modalImageWrapper.offsetLeft;
|
||
const y = e.pageY - modalImageWrapper.offsetTop;
|
||
const walkX = x - startX;
|
||
const walkY = y - startY;
|
||
modalImageWrapper.scrollLeft = scrollLeft - walkX;
|
||
modalImageWrapper.scrollTop = scrollTop - walkY;
|
||
});
|
||
|
||
modalImageWrapper.addEventListener('click', (e) => {
|
||
if (dragMoved) {
|
||
dragMoved = false;
|
||
return;
|
||
}
|
||
toggleZoom();
|
||
});
|
||
|
||
modalClose?.addEventListener('click', closeModal);
|
||
modalPrev?.addEventListener('click', showPrev);
|
||
modalNext?.addEventListener('click', showNext);
|
||
|
||
modal.addEventListener('click', (e) => {
|
||
if (e.target === modal) closeModal();
|
||
});
|
||
|
||
document.addEventListener('keydown', (e) => {
|
||
if (!modal.classList.contains('active')) return;
|
||
if (e.key === 'ArrowRight') showNext();
|
||
if (e.key === 'ArrowLeft') showPrev();
|
||
if (e.key === 'Escape') closeModal();
|
||
});
|
||
|
||
images.forEach((img, index) => {
|
||
img.addEventListener('click', () => openModal(index));
|
||
});
|
||
|
||
window.addEventListener('popstate', () => {
|
||
if (modal.classList.contains('active')) {
|
||
closeModal();
|
||
} else {
|
||
location.reload();
|
||
}
|
||
});
|
||
});
|