Files
swiper/js/history.js
2025-06-25 04:21:13 +01:00

491 lines
20 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = `<div class="no-selections">Loading selections...</div>`;
fetch('/selections')
.then(response => response.json())
.then(data => {
if (data.selections && data.selections.length > 0) {
cachedSelections = data.selections;
populateResolutionFilter(cachedSelections);
renderSelections();
} else {
selectionGrid.innerHTML = '<div class="no-selections">No selections found</div>';
}
})
.catch(error => {
console.error('Error loading selections:', error);
selectionGrid.innerHTML = `<div class="error">Error loading selections: ${error.message}</div>`;
});
};
const populateResolutionFilter = (selections) => {
const resolutions = [...new Set(selections.map(s => s.resolution))].sort();
// Populate select options (excluding duplicates)
resolutionSelect.innerHTML = '<option value="all" selected>All Resolutions</option>';
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 = '<div class="no-selections">No selections match the current filters</div>';
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 = `
<div class="selection-checkbox-container">
<input type="checkbox" class="selection-checkbox">
</div>
<img src="${ensureImagePath(selection.image_path)}" alt="Selected image" loading="lazy" class="${blurClass}">
<div class="selection-action action-${actionClass(selection.action)}"><img src="/static/icons/${actionIconMap[selection.action]}" alt="${selection.action}" class="selection-action"></div>
<div class="selection-info">
<p>${selection.image_path.split('/').pop()}</p>
<p>Resolution: ${selection.resolution}</p>
<p>Created: ${formatDate(selection.creation_date)}</p>
</div>
<div class="selection-controls">
<button class="control-btn edit-btn">Change</button>
<button class="control-btn delete-btn">Remove</button>
</div>
`;
selectionGrid.appendChild(item);
});
};
// ---- NSFW Blur Toggle ----
blurToggleBtn.addEventListener('click', () => {
blurNsfw = !blurNsfw;
blurToggleBtn.classList.toggle('active', blurNsfw);
renderSelections();
});
const actionClass = (action) => {
const map = { 'Discarded':'discard', 'Kept':'keep', 'Favourited':'favorite', 'Reviewing':'review' };
return map[action] || 'discard';
};
// Map action names to icon filenames for display
const actionIconMap = {
'Kept': 'keep.svg',
'Discarded': 'discard.svg',
'Favourited': 'fav.svg',
'Reviewing': 'review.svg'
};
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 = '&times;';
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 ? '&#9660;' : '&#9650;';
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();
});