// /app/static/js/view-modal.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 // --------------------------- // Use shared TagUtils module const { getTagIcon, getTagDisplayName, getTagDisplayNameForOverlay, escapeHtml, createTagChipHtml } = window.TagUtils || {}; 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 icon = getTagIcon(t.kind); const displayName = getTagDisplayName(t.name); chip.innerHTML = `${icon} ${displayName} `; 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) // --------------------------- 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 => createTagChipHtml(t)).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 = ''; 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); } // Fully reload series info to show updated management interface await loadSeriesInfo(imageId); } 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 // Auto-select if image has exactly one series tag const seriesTags = j.series_tags || []; if (seriesTags.length === 1) { showAddToSeries({ series_tag_id: seriesTags[0].id, page_number: 1 }); } else { 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 = '