updated view-modal generating js and references, corrected logging issue with archive import.
This commit is contained in:
@@ -0,0 +1,820 @@
|
||||
// /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;
|
||||
const tagAutocomplete = document.getElementById('tagAutocomplete');
|
||||
|
||||
// Series info elements (when image IS in a series)
|
||||
const seriesInfoEl = document.getElementById('modalSeriesInfo');
|
||||
const seriesNameEl = document.getElementById('modalSeriesName');
|
||||
const pageNumberInput = document.getElementById('modalPageNumber');
|
||||
const updatePageBtn = document.getElementById('updatePageBtn');
|
||||
const pageUpdateStatus = document.getElementById('pageUpdateStatus');
|
||||
let currentSeriesPageId = null;
|
||||
|
||||
// Add to series elements (when image is NOT in a series)
|
||||
const addToSeriesEl = document.getElementById('modalAddToSeries');
|
||||
const seriesSelect = document.getElementById('seriesSelect');
|
||||
const addSeriesPageNumber = document.getElementById('addSeriesPageNumber');
|
||||
const addToSeriesBtn = document.getElementById('addToSeriesBtn');
|
||||
const addToSeriesStatus = document.getElementById('addToSeriesStatus');
|
||||
let seriesTagsLoaded = false;
|
||||
|
||||
// Autocomplete state
|
||||
let autocompleteItems = [];
|
||||
let autocompleteSelectedIndex = -1;
|
||||
let autocompleteDebounce = 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;
|
||||
|
||||
// Legacy pagination inputs (for non-infinite-scroll pages)
|
||||
const hasNextPageInput = document.getElementById('hasNextPage');
|
||||
const nextPageUrlInput = document.getElementById('nextPageUrl');
|
||||
const hasPrevPageInput = document.getElementById('hasPrevPage');
|
||||
const prevPageUrlInput = document.getElementById('prevPageUrl');
|
||||
|
||||
// Check if we're in infinite scroll mode
|
||||
// Infinite scroll mode is when galleryState exists (set by gallery.html template)
|
||||
const isInfiniteScrollMode = typeof window.galleryState !== 'undefined';
|
||||
|
||||
// ---------------------------
|
||||
// Tag editor helpers
|
||||
// ---------------------------
|
||||
function setEditorImageId(id) {
|
||||
if (tagEditor) tagEditor.dataset.imageId = id || '';
|
||||
}
|
||||
function getEditorImageId() {
|
||||
return tagEditor ? tagEditor.dataset.imageId : '';
|
||||
}
|
||||
function getTagIcon(kind) {
|
||||
const icons = {
|
||||
artist: '🎨',
|
||||
archive: '🗜️',
|
||||
character: '👤',
|
||||
series: '📺',
|
||||
fandom: '🎭',
|
||||
rating: '⚠️',
|
||||
source: '🌐',
|
||||
post: '📌'
|
||||
};
|
||||
return icons[kind] || '#';
|
||||
}
|
||||
|
||||
function getTagDisplayName(name) {
|
||||
// Remove prefix if present (e.g., "artist:Name" -> "Name")
|
||||
return name.includes(':') ? name.split(':', 2)[1] : name;
|
||||
}
|
||||
|
||||
function renderTags(tags) {
|
||||
if (!tagList) return;
|
||||
tagList.innerHTML = '';
|
||||
(tags || []).forEach(t => {
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'tag-chip';
|
||||
const icon = getTagIcon(t.kind);
|
||||
const displayName = getTagDisplayName(t.name);
|
||||
chip.innerHTML = `${icon} ${displayName} <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
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Gallery item tag refresh (fallback if bulk-select.js not loaded)
|
||||
// ---------------------------
|
||||
function getTagDisplayNameForOverlay(name, kind) {
|
||||
if (['artist', 'character', 'series', 'fandom', 'rating'].includes(kind)) {
|
||||
return name.includes(':') ? name.split(':', 2)[1] : name;
|
||||
}
|
||||
return '#' + name;
|
||||
}
|
||||
|
||||
async function refreshItemTagsFallback(imageId) {
|
||||
const item = document.querySelector(`.img-clickable[data-id="${imageId}"]`);
|
||||
if (!item) return;
|
||||
|
||||
try {
|
||||
const r = await fetch(`/image/${imageId}/tags`);
|
||||
const data = await r.json();
|
||||
if (!data.ok) return;
|
||||
|
||||
const visibleTags = (data.tags || []).filter(t => t.kind !== 'archive');
|
||||
let overlay = item.querySelector('.tag-overlay');
|
||||
|
||||
if (visibleTags.length === 0) {
|
||||
if (overlay) overlay.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.className = 'tag-overlay';
|
||||
item.appendChild(overlay);
|
||||
}
|
||||
|
||||
overlay.innerHTML = visibleTags.map(t => {
|
||||
const displayName = getTagDisplayNameForOverlay(t.name, t.kind);
|
||||
const encodedName = encodeURIComponent(t.name);
|
||||
const escapedName = t.name.replace(/"/g, '"');
|
||||
const escapedDisplay = displayName.replace(/</g, '<').replace(/>/g, '>');
|
||||
return `<a class="tag-chip" href="/gallery?tag=${encodedName}" title="Filter by ${escapedName}" onclick="event.stopPropagation()">${escapedDisplay}</a>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
console.error('Failed to refresh tags for image', imageId, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Register fallback if bulk-select.js hasn't registered it
|
||||
if (!window.refreshItemTags) {
|
||||
window.refreshItemTags = refreshItemTagsFallback;
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Series info helpers
|
||||
// ---------------------------
|
||||
function hideSeriesInfo() {
|
||||
if (seriesInfoEl) seriesInfoEl.style.display = 'none';
|
||||
currentSeriesPageId = null;
|
||||
if (pageUpdateStatus) pageUpdateStatus.textContent = '';
|
||||
}
|
||||
|
||||
function hideAddToSeries() {
|
||||
if (addToSeriesEl) addToSeriesEl.style.display = 'none';
|
||||
if (addToSeriesStatus) addToSeriesStatus.textContent = '';
|
||||
}
|
||||
|
||||
async function loadSeriesTags() {
|
||||
if (!seriesSelect) return;
|
||||
if (seriesTagsLoaded) return Promise.resolve();
|
||||
try {
|
||||
// Fetch all series tags using the kind filter
|
||||
const r = await fetch('/api/tags/search?kind=series&limit=100');
|
||||
const j = await r.json();
|
||||
const seriesTags = j.tags || [];
|
||||
|
||||
// Clear existing options except the first placeholder
|
||||
seriesSelect.innerHTML = '<option value="">Select series...</option>';
|
||||
|
||||
seriesTags.forEach(tag => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = tag.id;
|
||||
// Display name without prefix
|
||||
opt.textContent = tag.name.includes(':') ? tag.name.split(':', 2)[1] : tag.name;
|
||||
seriesSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
seriesTagsLoaded = true;
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function showAddToSeries(seriesPage = null) {
|
||||
if (!addToSeriesEl) return;
|
||||
loadSeriesTags().then(() => {
|
||||
// Pre-fill if image is already in a series
|
||||
if (seriesPage && seriesSelect) {
|
||||
seriesSelect.value = seriesPage.series_tag_id;
|
||||
if (addSeriesPageNumber) addSeriesPageNumber.value = seriesPage.page_number;
|
||||
if (addToSeriesBtn) {
|
||||
addToSeriesBtn.disabled = false;
|
||||
addToSeriesBtn.textContent = 'Update';
|
||||
}
|
||||
} else {
|
||||
if (seriesSelect) seriesSelect.value = '';
|
||||
if (addSeriesPageNumber) addSeriesPageNumber.value = '1';
|
||||
if (addToSeriesBtn) {
|
||||
addToSeriesBtn.disabled = true;
|
||||
addToSeriesBtn.textContent = 'Add';
|
||||
}
|
||||
}
|
||||
});
|
||||
if (addToSeriesStatus) addToSeriesStatus.textContent = '';
|
||||
addToSeriesEl.style.display = 'block';
|
||||
}
|
||||
|
||||
async function handleAddToSeries() {
|
||||
const imageId = getEditorImageId();
|
||||
const seriesTagId = seriesSelect?.value;
|
||||
const pageNumber = parseInt(addSeriesPageNumber?.value);
|
||||
|
||||
if (!imageId || !seriesTagId || isNaN(pageNumber) || pageNumber < 1) return;
|
||||
|
||||
const isUpdate = addToSeriesBtn?.textContent === 'Update';
|
||||
if (addToSeriesBtn) addToSeriesBtn.disabled = true;
|
||||
if (addToSeriesStatus) addToSeriesStatus.textContent = isUpdate ? 'Updating...' : 'Adding...';
|
||||
|
||||
try {
|
||||
const r = await fetch(`/api/image/${imageId}/add-to-series`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ series_tag_id: parseInt(seriesTagId), page_number: pageNumber })
|
||||
});
|
||||
const j = await r.json();
|
||||
|
||||
if (j.ok) {
|
||||
if (addToSeriesStatus) {
|
||||
addToSeriesStatus.textContent = '✓ Saved';
|
||||
setTimeout(() => { addToSeriesStatus.textContent = ''; }, 2000);
|
||||
}
|
||||
// Update the series info section with new data
|
||||
showSeriesInfo(j.series_page);
|
||||
// Re-enable the button
|
||||
if (addToSeriesBtn) addToSeriesBtn.disabled = false;
|
||||
} else {
|
||||
if (addToSeriesStatus) addToSeriesStatus.textContent = j.error || 'Error';
|
||||
if (addToSeriesBtn) addToSeriesBtn.disabled = false;
|
||||
}
|
||||
} catch (e) {
|
||||
if (addToSeriesStatus) addToSeriesStatus.textContent = 'Error';
|
||||
if (addToSeriesBtn) addToSeriesBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showSeriesInfo(seriesPage) {
|
||||
if (!seriesInfoEl || !seriesPage) return;
|
||||
currentSeriesPageId = seriesPage.id;
|
||||
|
||||
// Display series name (strip prefix)
|
||||
const displayName = seriesPage.series_name.includes(':')
|
||||
? seriesPage.series_name.split(':', 1)[1]
|
||||
: seriesPage.series_name;
|
||||
if (seriesNameEl) {
|
||||
seriesNameEl.textContent = displayName;
|
||||
seriesNameEl.href = `/gallery?tag=${encodeURIComponent(seriesPage.series_name)}`;
|
||||
}
|
||||
|
||||
// Display page number
|
||||
if (pageNumberInput) {
|
||||
pageNumberInput.value = seriesPage.page_number;
|
||||
}
|
||||
|
||||
if (pageUpdateStatus) pageUpdateStatus.textContent = '';
|
||||
seriesInfoEl.style.display = 'block';
|
||||
}
|
||||
|
||||
async function loadSeriesInfo(imageId) {
|
||||
hideSeriesInfo();
|
||||
hideAddToSeries();
|
||||
if (!imageId) return;
|
||||
try {
|
||||
const r = await fetch(`/api/image/${imageId}/series-info`);
|
||||
const j = await r.json();
|
||||
if (j.ok && j.in_series) {
|
||||
// Show series info (with link) and pre-fill the dropdown
|
||||
showSeriesInfo(j.series_page);
|
||||
showAddToSeries(j.series_page);
|
||||
} else {
|
||||
// Show add-to-series option when not in a series
|
||||
showAddToSeries();
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePageNumber() {
|
||||
if (!currentSeriesPageId || !pageNumberInput) return;
|
||||
const newPageNumber = parseInt(pageNumberInput.value);
|
||||
if (isNaN(newPageNumber) || newPageNumber < 1) {
|
||||
if (pageUpdateStatus) pageUpdateStatus.textContent = 'Invalid';
|
||||
return;
|
||||
}
|
||||
|
||||
if (updatePageBtn) updatePageBtn.disabled = true;
|
||||
if (pageUpdateStatus) pageUpdateStatus.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('page_number', newPageNumber);
|
||||
const r = await fetch(`/api/series/page/${currentSeriesPageId}/update`, {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
});
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
if (pageUpdateStatus) {
|
||||
pageUpdateStatus.textContent = '✓';
|
||||
setTimeout(() => { pageUpdateStatus.textContent = ''; }, 2000);
|
||||
}
|
||||
} else {
|
||||
if (pageUpdateStatus) pageUpdateStatus.textContent = j.error || 'Error';
|
||||
}
|
||||
} catch (e) {
|
||||
if (pageUpdateStatus) pageUpdateStatus.textContent = 'Error';
|
||||
} finally {
|
||||
if (updatePageBtn) updatePageBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Wire up the save button
|
||||
if (updatePageBtn) {
|
||||
updatePageBtn.addEventListener('click', updatePageNumber);
|
||||
}
|
||||
|
||||
// Allow Enter key in page number input to save
|
||||
if (pageNumberInput) {
|
||||
pageNumberInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
updatePageNumber();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Enable/disable add button based on series selection
|
||||
if (seriesSelect) {
|
||||
seriesSelect.addEventListener('change', () => {
|
||||
if (addToSeriesBtn) {
|
||||
addToSeriesBtn.disabled = !seriesSelect.value;
|
||||
// Update button text: "Update" if image is in a series, "Add" if not
|
||||
if (seriesSelect.value && currentSeriesPageId) {
|
||||
addToSeriesBtn.textContent = 'Update';
|
||||
} else if (seriesSelect.value) {
|
||||
addToSeriesBtn.textContent = 'Add';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add to series button click
|
||||
if (addToSeriesBtn) {
|
||||
addToSeriesBtn.addEventListener('click', handleAddToSeries);
|
||||
}
|
||||
|
||||
// Allow Enter key in add-to-series page number input
|
||||
if (addSeriesPageNumber) {
|
||||
addSeriesPageNumber.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && seriesSelect?.value) {
|
||||
e.preventDefault();
|
||||
handleAddToSeries();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// ---------------------------
|
||||
// Autocomplete helpers
|
||||
// ---------------------------
|
||||
function hideAutocomplete() {
|
||||
if (tagAutocomplete) {
|
||||
tagAutocomplete.classList.remove('active');
|
||||
tagAutocomplete.innerHTML = '';
|
||||
}
|
||||
autocompleteItems = [];
|
||||
autocompleteSelectedIndex = -1;
|
||||
}
|
||||
|
||||
function positionAutocomplete() {
|
||||
if (!tagAutocomplete || !tagInput) return;
|
||||
const rect = tagInput.getBoundingClientRect();
|
||||
tagAutocomplete.style.top = `${rect.bottom + 4}px`;
|
||||
tagAutocomplete.style.left = `${rect.left}px`;
|
||||
tagAutocomplete.style.width = `${rect.width}px`;
|
||||
}
|
||||
|
||||
function renderAutocomplete(tags) {
|
||||
if (!tagAutocomplete) return;
|
||||
autocompleteItems = tags;
|
||||
autocompleteSelectedIndex = -1;
|
||||
|
||||
if (tags.length === 0) {
|
||||
tagAutocomplete.innerHTML = '<div class="tag-autocomplete-empty">No matching tags</div>';
|
||||
positionAutocomplete();
|
||||
tagAutocomplete.classList.add('active');
|
||||
return;
|
||||
}
|
||||
|
||||
tagAutocomplete.innerHTML = tags.map((t, i) => {
|
||||
const icon = getTagIcon(t.kind);
|
||||
const displayName = getTagDisplayName(t.name);
|
||||
return `<div class="tag-autocomplete-item" data-index="${i}" data-name="${t.name}">
|
||||
<span>${icon} ${displayName}</span>
|
||||
<span class="tag-kind">${t.kind || 'user'}</span>
|
||||
</div>`;
|
||||
}).join('');
|
||||
positionAutocomplete();
|
||||
tagAutocomplete.classList.add('active');
|
||||
}
|
||||
|
||||
function selectAutocompleteItem(index) {
|
||||
const items = tagAutocomplete?.querySelectorAll('.tag-autocomplete-item');
|
||||
if (!items) return;
|
||||
items.forEach((el, i) => {
|
||||
el.classList.toggle('selected', i === index);
|
||||
});
|
||||
autocompleteSelectedIndex = index;
|
||||
}
|
||||
|
||||
async function fetchAutocomplete(query) {
|
||||
try {
|
||||
// Exclude system-managed tag kinds from autocomplete (archive, post, source)
|
||||
const excludeKinds = 'archive,post,source';
|
||||
const url = query
|
||||
? `/api/tags/search?q=${encodeURIComponent(query)}&limit=8&exclude_kind=${excludeKinds}`
|
||||
: `/api/tags/search?limit=8&exclude_kind=${excludeKinds}`;
|
||||
const r = await fetch(url);
|
||||
const j = await r.json();
|
||||
renderAutocomplete(j.tags || []);
|
||||
} catch {
|
||||
hideAutocomplete();
|
||||
}
|
||||
}
|
||||
|
||||
// Shared function to submit a tag (avoids page refresh issues with form dispatch)
|
||||
async function submitTag() {
|
||||
const id = getEditorImageId();
|
||||
const name = (tagInput?.value || '').trim();
|
||||
if (!id || !name) return;
|
||||
hideAutocomplete();
|
||||
await addTag(id, name);
|
||||
if (tagInput) tagInput.value = '';
|
||||
}
|
||||
|
||||
if (tagInput) {
|
||||
// Show autocomplete on focus
|
||||
tagInput.addEventListener('focus', () => {
|
||||
const val = tagInput.value.trim();
|
||||
fetchAutocomplete(val);
|
||||
});
|
||||
|
||||
// Hide autocomplete on blur (with delay for click)
|
||||
tagInput.addEventListener('blur', () => {
|
||||
setTimeout(hideAutocomplete, 200);
|
||||
});
|
||||
|
||||
// Search as user types
|
||||
tagInput.addEventListener('input', () => {
|
||||
clearTimeout(autocompleteDebounce);
|
||||
autocompleteDebounce = setTimeout(() => {
|
||||
fetchAutocomplete(tagInput.value.trim());
|
||||
}, 150);
|
||||
});
|
||||
|
||||
// Keyboard navigation
|
||||
tagInput.addEventListener('keydown', (e) => {
|
||||
if (!tagAutocomplete?.classList.contains('active')) return;
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
const nextIndex = Math.min(autocompleteSelectedIndex + 1, autocompleteItems.length - 1);
|
||||
selectAutocompleteItem(nextIndex);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
const prevIndex = Math.max(autocompleteSelectedIndex - 1, 0);
|
||||
selectAutocompleteItem(prevIndex);
|
||||
} else if (e.key === 'Enter' && autocompleteSelectedIndex >= 0) {
|
||||
e.preventDefault();
|
||||
const selected = autocompleteItems[autocompleteSelectedIndex];
|
||||
if (selected) {
|
||||
tagInput.value = selected.name;
|
||||
submitTag();
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
hideAutocomplete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Click on autocomplete item - use mousedown to fire before blur
|
||||
if (tagAutocomplete) {
|
||||
tagAutocomplete.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault(); // Prevent blur from firing
|
||||
e.stopPropagation();
|
||||
const item = e.target.closest('.tag-autocomplete-item');
|
||||
if (!item) return;
|
||||
const name = item.dataset.name;
|
||||
if (tagInput && name) {
|
||||
tagInput.value = name;
|
||||
submitTag();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (tagForm) {
|
||||
tagForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
await submitTag();
|
||||
});
|
||||
}
|
||||
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);
|
||||
loadSeriesInfo(imageId);
|
||||
}
|
||||
|
||||
function openModal(index) {
|
||||
modal.classList.add('active');
|
||||
updateImage(index);
|
||||
history.replaceState({ modalIndex: index }, '', window.location.href);
|
||||
// Focus the tag input after modal opens (desktop only - avoids keyboard popup on mobile)
|
||||
if (tagInput && !isTouchDevice()) {
|
||||
setTimeout(() => tagInput.focus(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Detect touch devices to avoid auto-focus keyboard popup
|
||||
function isTouchDevice() {
|
||||
return ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
// Get the image ID before clearing so we can refresh its tags
|
||||
const imageId = getEditorImageId();
|
||||
|
||||
modal.classList.remove('active');
|
||||
modalImageWrapper.innerHTML = '';
|
||||
currentIndex = -1;
|
||||
resetZoom();
|
||||
// clear tag UI
|
||||
setEditorImageId('');
|
||||
renderTags([]);
|
||||
hideSeriesInfo();
|
||||
hideAddToSeries();
|
||||
history.replaceState({}, '', window.location.pathname + window.location.search);
|
||||
|
||||
// Refresh the gallery item's tags display (if function available from bulk-select.js)
|
||||
if (imageId && window.refreshItemTags) {
|
||||
window.refreshItemTags(imageId);
|
||||
}
|
||||
}
|
||||
|
||||
function showNext() {
|
||||
if (currentIndex < images.length - 1) {
|
||||
updateImage(currentIndex + 1);
|
||||
} else if (isInfiniteScrollMode) {
|
||||
// In infinite scroll mode, just stay at the last image
|
||||
// User needs to scroll the gallery to load more
|
||||
return;
|
||||
} else if (hasNextPageInput?.value === "true") {
|
||||
// Legacy pagination mode - fetch next page
|
||||
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 (isInfiniteScrollMode) {
|
||||
// In infinite scroll mode, just stay at the first image
|
||||
return;
|
||||
} else if (hasPrevPageInput?.value === "true") {
|
||||
// Legacy pagination mode - fetch previous page
|
||||
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();
|
||||
});
|
||||
|
||||
// Use event delegation on the document for .img-clickable clicks
|
||||
// This allows dynamically added elements (e.g., from shuffle) to work
|
||||
document.addEventListener('click', (e) => {
|
||||
const clickable = e.target.closest('.img-clickable');
|
||||
if (!clickable) return;
|
||||
|
||||
// Re-query images to get current state (may have changed from shuffle)
|
||||
images = Array.from(document.querySelectorAll('.img-clickable'));
|
||||
const index = images.indexOf(clickable);
|
||||
if (index !== -1) {
|
||||
openModal(index);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for showcase updates to refresh the images array
|
||||
document.addEventListener('showcaseUpdated', () => {
|
||||
images = Array.from(document.querySelectorAll('.img-clickable'));
|
||||
});
|
||||
|
||||
// Listen for gallery infinite scroll updates to refresh the images array
|
||||
document.addEventListener('galleryUpdated', () => {
|
||||
images = Array.from(document.querySelectorAll('.img-clickable'));
|
||||
});
|
||||
|
||||
window.addEventListener('popstate', () => {
|
||||
if (modal.classList.contains('active')) {
|
||||
closeModal();
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user