Basically wrote the whole thing.

This commit is contained in:
Aodhan
2025-06-25 04:21:13 +01:00
parent 1ff4a6f6d7
commit c5391a957d
216 changed files with 168676 additions and 1303 deletions

View File

@@ -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 = '&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();
});
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 ? '&#9660;' : '&#9650;';
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();
});