Basically wrote the whole thing.
This commit is contained in:
363
js/history.js
363
js/history.js
@@ -1,12 +1,36 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const selectionGrid = document.getElementById('selection-grid');
|
||||
// Unified filter state
|
||||
// Unified filter state supporting multi-selection for action and orientation
|
||||
const filterState = {
|
||||
action: 'all',
|
||||
orientation: 'all',
|
||||
resolution: 'all'
|
||||
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 resolutionFilter = document.getElementById('resolution-filter');
|
||||
const selectAllBtn = document.getElementById('select-all');
|
||||
const deselectAllBtn = document.getElementById('deselect-all');
|
||||
const downloadSelectedBtn = document.getElementById('download-selected');
|
||||
@@ -18,14 +42,26 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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>`;
|
||||
@@ -49,23 +85,58 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
const populateResolutionFilter = (selections) => {
|
||||
const resolutions = [...new Set(selections.map(s => s.resolution))].sort();
|
||||
resolutionFilter.innerHTML = '<option value="all">All Resolutions</option>';
|
||||
resolutions.forEach(resolution => {
|
||||
const option = document.createElement('option');
|
||||
option.value = resolution;
|
||||
option.textContent = resolution;
|
||||
resolutionFilter.appendChild(option);
|
||||
// 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 = '';
|
||||
const selections = cachedSelections;
|
||||
let selections = sortSelections(cachedSelections);
|
||||
|
||||
const filteredSelections = selections.filter(s =>
|
||||
(filterState.action === 'all' || s.action === filterState.action) &&
|
||||
(filterState.orientation === 'all' || s.orientation === filterState.orientation) &&
|
||||
(filterState.resolution === 'all' || s.resolution === filterState.resolution)
|
||||
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) {
|
||||
@@ -73,7 +144,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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;
|
||||
@@ -81,11 +159,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
<div class="selection-checkbox-container">
|
||||
<input type="checkbox" class="selection-checkbox">
|
||||
</div>
|
||||
<img src="${selection.image_path}" alt="Selected image" loading="lazy">
|
||||
<div class="selection-action action-${actionClass(selection.action)}">${selection.action}</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>
|
||||
@@ -94,17 +173,37 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
`;
|
||||
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';
|
||||
};
|
||||
|
||||
const actionClass = (action) => {
|
||||
const map = { 'Discard':'discard', 'Keep':'keep', 'Favorite':'favorite', 'Review':'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('.label').textContent = selectedItems.length > 0 ? `Download (${selectedItems.length})` : 'Download';
|
||||
downloadSelectedBtn.querySelector('.count').textContent = selectedItems.length > 0 ? selectedItems.length : '0';
|
||||
};
|
||||
|
||||
selectionGrid.addEventListener('click', (e) => {
|
||||
@@ -132,6 +231,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
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';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -141,22 +251,131 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
if (!btn) return;
|
||||
|
||||
// Determine filter type and value
|
||||
const { filter, orientation } = btn.dataset;
|
||||
const { filter, orientation, nsfw } = btn.dataset;
|
||||
// Action filters
|
||||
if (filter !== undefined) {
|
||||
filterState.action = filter;
|
||||
// update active classes within the same group
|
||||
btn.parentElement.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||||
toggleFilter(btn, 'actions', filter);
|
||||
}
|
||||
// Orientation filters
|
||||
if (orientation !== undefined) {
|
||||
filterState.orientation = orientation;
|
||||
btn.parentElement.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
||||
toggleFilter(btn, 'orientations', orientation);
|
||||
}
|
||||
btn.classList.add('active');
|
||||
// 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();
|
||||
});
|
||||
|
||||
resolutionFilter.addEventListener('change', function() {
|
||||
filterState.resolution = this.value;
|
||||
// ---------------- Sorting controls ----------------
|
||||
sortFieldSelect.addEventListener('change', () => {
|
||||
sortState.field = sortFieldSelect.value;
|
||||
renderSelections();
|
||||
});
|
||||
|
||||
sortDirBtn.addEventListener('click', () => {
|
||||
sortState.desc = !sortState.desc;
|
||||
sortDirBtn.innerHTML = sortState.desc ? '▼' : '▲';
|
||||
renderSelections();
|
||||
});
|
||||
|
||||
@@ -182,16 +401,90 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
closeActionModal.addEventListener('click', () => actionModal.style.display = 'none');
|
||||
|
||||
actionButtons.forEach(button => button.addEventListener('click', function() {
|
||||
const action = this.dataset.action;
|
||||
// Implement update action functionality
|
||||
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', () => {
|
||||
// Implement reset database functionality
|
||||
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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user