document.addEventListener('DOMContentLoaded', function() { const selectionGrid = document.getElementById('selection-grid'); // Unified filter state supporting multi-selection for action and orientation const filterState = { actions: new Set(['all']), // Set of selected action filters orientations: new Set(['all']), // Set of selected orientation filters resolutions: new Set(['all']), nsfw: new Set(['all']) }; // Blur state for NSFW thumbnails (on by default) let blurNsfw = true; const resolutionSelect = document.getElementById('resolution-select'); // Search controls const searchInput = document.getElementById('search-input'); const searchButton = document.getElementById('search-button'); const keywordPillsContainer = document.querySelector('.keyword-pills-container'); const resolutionPillsContainer = document.querySelector('.resolution-pills'); const blurToggleBtn = document.getElementById('toggle-blur'); // initialise button state blurToggleBtn.classList.toggle('active', blurNsfw); // Sorting controls const sortFieldSelect = document.getElementById('sort-field'); const sortDirBtn = document.getElementById('sort-direction'); // Sort state const sortState = { // Keyword search state searchKeywords: [], field: 'swipe', // 'swipe' | 'created' | 'width' | 'height' desc: true, }; const selectAllBtn = document.getElementById('select-all'); const deselectAllBtn = document.getElementById('deselect-all'); const downloadSelectedBtn = document.getElementById('download-selected'); const actionModal = document.getElementById('action-modal'); const closeActionModal = document.getElementById('close-action-modal'); const actionButtons = actionModal.querySelectorAll('.action-btn'); const modalPreviewImg = document.getElementById('modal-preview-img'); const modalMessage = document.getElementById('modal-message'); const resetBtn = document.getElementById('reset-db'); // View modal elements const viewModal = document.getElementById('view-modal'); const viewImg = document.getElementById('view-image'); const viewFilename = document.getElementById('view-filename'); const viewResolution = document.getElementById('view-resolution'); const viewCreated = document.getElementById('view-created'); const viewPrompt = document.getElementById('view-prompt'); const closeViewModal = document.getElementById('close-view-modal'); const resetModal = document.getElementById('reset-modal'); const confirmResetBtn = document.getElementById('confirm-reset'); const cancelResetBtn = document.getElementById('cancel-reset'); const resetMessage = document.getElementById('reset-message'); let cachedSelections = []; // blurNsfw already defined above let selectedItems = []; let currentSelectionId = null; // Helper to ensure correct /images/ prefix const ensureImagePath = (p) => p.startsWith('/images/') ? p : `/images/${p.replace(/^\/+/,'')}`; const loadSelections = () => { selectionGrid.innerHTML = `
Loading selections...
`; fetch('/selections') .then(response => response.json()) .then(data => { if (data.selections && data.selections.length > 0) { cachedSelections = data.selections; populateResolutionFilter(cachedSelections); renderSelections(); } else { selectionGrid.innerHTML = '
No selections found
'; } }) .catch(error => { console.error('Error loading selections:', error); selectionGrid.innerHTML = `
Error loading selections: ${error.message}
`; }); }; const populateResolutionFilter = (selections) => { const resolutions = [...new Set(selections.map(s => s.resolution))].sort(); // Populate select options (excluding duplicates) resolutionSelect.innerHTML = ''; resolutions.forEach(res => { const opt = document.createElement('option'); opt.value = res; opt.textContent = res; resolutionSelect.appendChild(opt); }); }; const sortSelections = (arr) => { const { field, desc } = sortState; const dir = desc ? -1 : 1; return [...arr].sort((a, b) => { let va, vb; switch (field) { case 'swipe': va = a.timestamp || 0; vb = b.timestamp || 0; break; case 'created': va = a.creation_date || 0; vb = b.creation_date || 0; break; case 'width': va = a.resolution_x || 0; vb = b.resolution_x || 0; break; case 'height': va = a.resolution_y || 0; vb = b.resolution_y || 0; break; default: va = 0; vb = 0; } return va === vb ? 0 : (va > vb ? dir : -dir); }); }; const renderSelections = () => { selectionGrid.innerHTML = ''; let selections = sortSelections(cachedSelections); const filteredSelections = selections.filter(s => (filterState.actions.has('all') || filterState.actions.has(s.action)) && (filterState.orientations.has('all') || filterState.orientations.has(s.orientation)) && (filterState.resolutions.has('all') || filterState.resolutions.has(s.resolution)) && ((filterState.nsfw.has('all')) || (filterState.nsfw.has('nsfw') && s.nsfw) || (filterState.nsfw.has('sfw') && !s.nsfw)) && (sortState.searchKeywords.length === 0 || (() => { const haystack = `${s.image_path} ${(s.prompt_data || '')} ${(s.positive_prompt || '')} ${(s.negative_prompt || '')}`.toLowerCase(); return sortState.searchKeywords.every(k => haystack.includes(k.toLowerCase())); })()) ); if (filteredSelections.length === 0) { selectionGrid.innerHTML = '
No selections match the current filters
'; return; } // Ensure image path is absolute and prefixed with /images/ const ensureImagePath = (p) => { if (p.startsWith('/images/')) return p; return `/images/${p.replace(/^\/+/, '')}`; }; filteredSelections.forEach(selection => { const blurClass = (blurNsfw && selection.nsfw) ? 'nsfw-blur' : ''; const item = document.createElement('div'); item.className = 'selection-item'; item.dataset.id = selection.id; item.innerHTML = `
Selected image
${selection.action}

${selection.image_path.split('/').pop()}

Resolution: ${selection.resolution}

Created: ${formatDate(selection.creation_date)}

`; selectionGrid.appendChild(item); }); }; // ---- NSFW Blur Toggle ---- blurToggleBtn.addEventListener('click', () => { blurNsfw = !blurNsfw; blurToggleBtn.classList.toggle('active', blurNsfw); renderSelections(); }); const actionClass = (action) => { // Direct mapping for full action names const map = { 'Discarded':'discard', 'Kept':'keep', 'Favourited':'favorite', 'Reviewing':'review' }; // Check direct mapping first if (map[action]) { return map[action]; } // Backwards compatibility: check first letter for single-letter actions and legacy actions const firstLetter = action ? action.charAt(0).toUpperCase() : ''; switch (firstLetter) { case 'K': return 'keep'; // Keep/Kept case 'D': return 'discard'; // Discard/Discarded case 'F': return 'favorite'; // Favorite/Favourited case 'R': return 'review'; // Review/Reviewed/Reviewing default: return 'discard'; // Default fallback } }; // Map action names to icon filenames for display const actionIconMap = { 'Kept': 'keep.svg', 'Discarded': 'discard.svg', 'Favourited': 'fav.svg', 'Reviewing': 'review.svg' }; // Get appropriate icon with backwards compatibility const getActionIcon = (action) => { // Check direct mapping first if (actionIconMap[action]) { return actionIconMap[action]; } // Backwards compatibility: check first letter const firstLetter = action ? action.charAt(0).toUpperCase() : ''; switch (firstLetter) { case 'K': return 'keep.svg'; // Keep/Kept case 'D': return 'discard.svg'; // Discard/Discarded case 'F': return 'fav.svg'; // Favorite/Favourited case 'R': return 'review.svg'; // Review/Reviewed/Reviewing default: return 'discard.svg'; // Default fallback } }; const getActionName = (action) => action; const formatDate = (ts) => { if (!ts) return 'N/A'; return new Date(ts * 1000).toLocaleDateString('en-GB'); // Day/Month/Year }; const updateDownloadButton = () => { downloadSelectedBtn.disabled = selectedItems.length === 0; downloadSelectedBtn.querySelector('.count').textContent = selectedItems.length > 0 ? selectedItems.length : '0'; }; selectionGrid.addEventListener('click', (e) => { const target = e.target; const selectionItem = target.closest('.selection-item'); if (!selectionItem) return; const selectionId = selectionItem.dataset.id; const selection = { id: selectionId, image_path: selectionItem.querySelector('img').src }; if (target.classList.contains('selection-checkbox')) { if (target.checked) { selectionItem.classList.add('selected'); selectedItems.push(selection); } else { selectionItem.classList.remove('selected'); selectedItems = selectedItems.filter(item => item.id !== selectionId); } updateDownloadButton(); } else if (target.classList.contains('edit-btn')) { currentSelectionId = selectionId; modalPreviewImg.src = selection.image_path; actionModal.style.display = 'flex'; } else if (target.classList.contains('delete-btn')) { if (confirm('Are you sure you want to delete this selection?')) { // Implement delete functionality } } else { // Open view modal when clicking elsewhere on the card/image const info = cachedSelections.find(s => String(s.id) === String(selectionId)); if (info) { viewImg.src = ensureImagePath(info.image_path); viewFilename.textContent = `File: ${info.image_path.split('/').pop()}`; viewResolution.textContent = `Resolution: ${info.resolution}`; viewCreated.textContent = `Created: ${formatDate(info.creation_date)}`; viewPrompt.textContent = info.prompt_data || info.positive_prompt || info.negative_prompt || 'N/A'; viewModal.style.display = 'flex'; } } }); // Delegated click handler for any filter button document.querySelector('.filter-container').addEventListener('click', (e) => { const btn = e.target.closest('.filter-btn'); if (!btn) return; // Determine filter type and value const { filter, orientation, nsfw } = btn.dataset; // Action filters if (filter !== undefined) { toggleFilter(btn, 'actions', filter); } // Orientation filters if (orientation !== undefined) { toggleFilter(btn, 'orientations', orientation); } // NSFW filters if (nsfw !== undefined) { toggleFilter(btn, 'nsfw', nsfw); } renderSelections(); // Helper to toggle filter selections function toggleFilter(button, key, value) { const set = filterState[key]; const groupButtons = button.parentElement.querySelectorAll('.filter-btn'); if (value === 'all') { // Selecting 'all' clears other selections set.clear(); set.add('all'); } else { if (set.has('all')) set.delete('all'); if (set.has(value)) { set.delete(value); } else { set.add(value); } if (set.size === 0) set.add('all'); } // Update active classes groupButtons.forEach(b => { const v = b.dataset.filter || b.dataset.orientation || b.dataset.nsfw; b.classList.toggle('active', set.has(v)); }); } }); // ---------------- Search keyword handling ---------------- const renderKeywordPills = () => { keywordPillsContainer.innerHTML = ''; sortState.searchKeywords.forEach(keyword => { const pill = document.createElement('div'); pill.className = 'keyword-pill'; pill.textContent = keyword; const removeBtn = document.createElement('button'); removeBtn.className = 'remove-keyword'; removeBtn.innerHTML = '×'; removeBtn.dataset.keyword = keyword; pill.appendChild(removeBtn); keywordPillsContainer.appendChild(pill); }); }; const addSearchKeyword = () => { const newKeyword = searchInput.value.trim(); if (newKeyword && !sortState.searchKeywords.includes(newKeyword)) { sortState.searchKeywords.push(newKeyword); renderKeywordPills(); renderSelections(); } searchInput.value = ''; searchInput.focus(); }; searchButton.addEventListener('click', addSearchKeyword); searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') addSearchKeyword(); }); keywordPillsContainer.addEventListener('click', (e) => { if (e.target.classList.contains('remove-keyword')) { const keywordToRemove = e.target.dataset.keyword; sortState.searchKeywords = sortState.searchKeywords.filter(k => k !== keywordToRemove); renderKeywordPills(); renderSelections(); } }); // ---------------- Resolution dropdown & pills ---------------- const addResolutionPill = (value) => { const pill = document.createElement('span'); pill.className = 'keyword-pill'; pill.textContent = value; const x = document.createElement('span'); x.className = 'remove-keyword'; x.textContent = '×'; pill.appendChild(x); resolutionPillsContainer.appendChild(pill); pill.addEventListener('click', () => { filterState.resolutions.delete(value); pill.remove(); if (filterState.resolutions.size === 0) filterState.resolutions.add('all'); renderSelections(); }); }; resolutionSelect.addEventListener('change', () => { const val = resolutionSelect.value; if (val === 'all') { filterState.resolutions.clear(); filterState.resolutions.add('all'); resolutionPillsContainer.innerHTML = ''; } else if (!filterState.resolutions.has(val)) { if (filterState.resolutions.has('all')) filterState.resolutions.delete('all'); filterState.resolutions.add(val); addResolutionPill(val); } resolutionSelect.value = 'all'; renderSelections(); }); // ---------------- Sorting controls ---------------- sortFieldSelect.addEventListener('change', () => { sortState.field = sortFieldSelect.value; renderSelections(); }); sortDirBtn.addEventListener('click', () => { sortState.desc = !sortState.desc; sortDirBtn.innerHTML = sortState.desc ? '▼' : '▲'; renderSelections(); }); selectAllBtn.addEventListener('click', () => { document.querySelectorAll('.selection-checkbox').forEach(cb => cb.checked = true); selectedItems = Array.from(document.querySelectorAll('.selection-item')).map(item => ({id: item.dataset.id, image_path: item.querySelector('img').src})); document.querySelectorAll('.selection-item').forEach(item => item.classList.add('selected')); updateDownloadButton(); }); deselectAllBtn.addEventListener('click', () => { document.querySelectorAll('.selection-checkbox').forEach(cb => cb.checked = false); selectedItems = []; document.querySelectorAll('.selection-item').forEach(item => item.classList.remove('selected')); updateDownloadButton(); }); downloadSelectedBtn.addEventListener('click', () => { const paths = selectedItems.map(item => item.image_path); const query = paths.map(p => `paths=${encodeURIComponent(p)}`).join('&'); window.location.href = `/download-selected?${query}`; }); closeActionModal.addEventListener('click', () => actionModal.style.display = 'none'); actionButtons.forEach(button => button.addEventListener('click', async function() { const direction = this.dataset.action; const actionMap = { left: 'Discarded', right: 'Kept', up: 'Favourited', down: 'Reviewing' }; const actionName = actionMap[direction] || direction; if (!currentSelectionId) return; // Show loading state modalMessage.textContent = 'Updating...'; modalMessage.style.color = '#ffffff'; try { const response = await fetch('/update-selection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: currentSelectionId, action: actionName }) }); const data = await response.json(); if (response.ok && data.success) { // Update cached selection locally const sel = cachedSelections.find(s => String(s.id) === String(currentSelectionId)); if (sel) sel.action = actionName; // Refresh grid to reflect changes renderSelections(); // Success feedback modalMessage.textContent = 'Action updated!'; modalMessage.style.color = '#2ecc71'; // Close modal after short delay setTimeout(() => { actionModal.style.display = 'none'; modalMessage.textContent = ''; }, 800); } else { throw new Error(data.message || 'Failed to update'); } } catch (err) { console.error('Error updating action:', err); modalMessage.textContent = `Error: ${err.message}`; modalMessage.style.color = '#e74c3c'; } })); resetBtn.addEventListener('click', () => resetModal.style.display = 'flex'); confirmResetBtn.addEventListener('click', () => { resetMessage.textContent = ''; confirmResetBtn.disabled = true; fetch('/reset-database', { method: 'POST', }) .then(response => response.json()) .then(data => { if (data.status === 'success') { resetMessage.textContent = 'Database reset successfully!'; resetMessage.style.color = 'green'; setTimeout(() => { resetModal.style.display = 'none'; loadSelections(); confirmResetBtn.disabled = false; }, 1500); } else { throw new Error(data.message || 'An unknown error occurred.'); } }) .catch(error => { console.error('Error resetting database:', error); resetMessage.textContent = `Error: ${error.message}`; resetMessage.style.color = 'red'; confirmResetBtn.disabled = false; }); }); cancelResetBtn.addEventListener('click', () => resetModal.style.display = 'none'); // ---- View modal close ---- closeViewModal.addEventListener('click', () => viewModal.style.display = 'none'); viewModal.addEventListener('click', (e) => { if (e.target === viewModal) viewModal.style.display = 'none'; }); document.addEventListener('keydown', e => { if (e.key === 'Escape' && viewModal.style.display === 'flex') { viewModal.style.display = 'none'; } }); loadSelections(); });