major updates to theming, creation of showcase view and polish of existing systems including tagging editting.
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
// /app/static/js/showcase.js
|
||||
// JS-managed masonry with column distribution for infinite scroll
|
||||
|
||||
(function() {
|
||||
const container = document.getElementById('showcaseGrid');
|
||||
if (!container) return;
|
||||
|
||||
// Configuration
|
||||
const DESKTOP_COLUMNS = 4;
|
||||
const MOBILE_COLUMNS = 2;
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
const SCROLL_THRESHOLD = 1500; // Load more when 1500px from bottom (about 2-3 rows ahead)
|
||||
const LOAD_BATCH_SIZE = 12;
|
||||
|
||||
// State
|
||||
let columns = [];
|
||||
let columnHeights = [];
|
||||
let isLoading = false;
|
||||
let currentColumnCount = 0;
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
setupColumns();
|
||||
loadInitialImages();
|
||||
setupEventListeners();
|
||||
}
|
||||
|
||||
function getColumnCount() {
|
||||
return window.innerWidth < MOBILE_BREAKPOINT ? MOBILE_COLUMNS : DESKTOP_COLUMNS;
|
||||
}
|
||||
|
||||
function setupColumns() {
|
||||
const count = getColumnCount();
|
||||
if (count === currentColumnCount && columns.length > 0) return;
|
||||
|
||||
currentColumnCount = count;
|
||||
container.innerHTML = '';
|
||||
columns = [];
|
||||
columnHeights = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const col = document.createElement('div');
|
||||
col.className = 'masonry-column';
|
||||
col.dataset.column = i;
|
||||
container.appendChild(col);
|
||||
columns.push(col);
|
||||
columnHeights.push(0);
|
||||
}
|
||||
}
|
||||
|
||||
function loadInitialImages() {
|
||||
const dataScript = document.getElementById('initialImages');
|
||||
if (!dataScript) return;
|
||||
|
||||
try {
|
||||
const images = JSON.parse(dataScript.textContent);
|
||||
distributeImages(images, true); // Use lazy loading for initial images (some may be below fold)
|
||||
} catch (e) {
|
||||
console.error('Failed to parse initial images:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function setupEventListeners() {
|
||||
// Keyboard shuffle
|
||||
document.addEventListener('keydown', (e) => {
|
||||
const modal = document.getElementById('imageModal');
|
||||
if (modal && modal.classList.contains('active')) return;
|
||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
||||
|
||||
if (e.key === 'r' || e.key === 'R' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
shuffle();
|
||||
}
|
||||
});
|
||||
|
||||
// Infinite scroll
|
||||
let scrollTimeout = null;
|
||||
window.addEventListener('scroll', () => {
|
||||
if (scrollTimeout) return;
|
||||
scrollTimeout = setTimeout(() => {
|
||||
scrollTimeout = null;
|
||||
checkScrollPosition();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Window resize - redistribute if column count changes
|
||||
let resizeTimeout = null;
|
||||
window.addEventListener('resize', () => {
|
||||
if (resizeTimeout) clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(() => {
|
||||
const newCount = getColumnCount();
|
||||
if (newCount !== currentColumnCount) {
|
||||
redistributeAllImages();
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
function checkScrollPosition() {
|
||||
const scrollBottom = window.innerHeight + window.scrollY;
|
||||
const docHeight = document.documentElement.scrollHeight;
|
||||
|
||||
if (docHeight - scrollBottom < SCROLL_THRESHOLD) {
|
||||
loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
function getShortestColumnIndex() {
|
||||
let minHeight = Infinity;
|
||||
let minIndex = 0;
|
||||
for (let i = 0; i < columnHeights.length; i++) {
|
||||
if (columnHeights[i] < minHeight) {
|
||||
minHeight = columnHeights[i];
|
||||
minIndex = i;
|
||||
}
|
||||
}
|
||||
return minIndex;
|
||||
}
|
||||
|
||||
function createImageElement(img, useLazyLoading = false) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'masonry-item img-clickable';
|
||||
item.dataset.id = img.id;
|
||||
item.dataset.full = img.full_url;
|
||||
item.dataset.type = img.is_video ? 'video' : 'image';
|
||||
item.dataset.filename = img.filename;
|
||||
|
||||
const imgEl = document.createElement('img');
|
||||
imgEl.src = img.thumb_url;
|
||||
imgEl.alt = img.filename;
|
||||
if (useLazyLoading) {
|
||||
imgEl.loading = 'lazy';
|
||||
}
|
||||
item.appendChild(imgEl);
|
||||
|
||||
if (img.tags && img.tags.length > 0) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'tag-overlay';
|
||||
img.tags.forEach(t => {
|
||||
const chip = document.createElement('a');
|
||||
chip.className = 'tag-chip';
|
||||
chip.href = `/gallery?tag=${encodeURIComponent(t.name)}`;
|
||||
chip.title = `Filter by ${t.name}`;
|
||||
chip.onclick = (e) => e.stopPropagation();
|
||||
chip.innerHTML = formatTag(t);
|
||||
overlay.appendChild(chip);
|
||||
});
|
||||
item.appendChild(overlay);
|
||||
}
|
||||
|
||||
if (img.is_video) {
|
||||
const play = document.createElement('div');
|
||||
play.className = 'play-overlay';
|
||||
play.textContent = '▶';
|
||||
item.appendChild(play);
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
function distributeImages(images, useLazyLoading = false) {
|
||||
images.forEach(img => {
|
||||
const colIndex = getShortestColumnIndex();
|
||||
const item = createImageElement(img, useLazyLoading);
|
||||
columns[colIndex].appendChild(item);
|
||||
|
||||
// Update height estimate (will be corrected after image loads)
|
||||
columnHeights[colIndex] += 300;
|
||||
|
||||
// Correct height after image loads
|
||||
const imgEl = item.querySelector('img');
|
||||
imgEl.onload = () => {
|
||||
updateColumnHeights();
|
||||
};
|
||||
});
|
||||
|
||||
document.dispatchEvent(new CustomEvent('showcaseUpdated'));
|
||||
}
|
||||
|
||||
function updateColumnHeights() {
|
||||
columns.forEach((col, i) => {
|
||||
columnHeights[i] = col.offsetHeight;
|
||||
});
|
||||
}
|
||||
|
||||
function redistributeAllImages() {
|
||||
// Collect all current items
|
||||
const allItems = [];
|
||||
columns.forEach(col => {
|
||||
Array.from(col.children).forEach(item => {
|
||||
allItems.push(item);
|
||||
});
|
||||
});
|
||||
|
||||
// Reset columns
|
||||
setupColumns();
|
||||
|
||||
// Re-add items maintaining order
|
||||
allItems.forEach(element => {
|
||||
const colIndex = getShortestColumnIndex();
|
||||
columns[colIndex].appendChild(element);
|
||||
columnHeights[colIndex] += element.offsetHeight || 300;
|
||||
});
|
||||
|
||||
document.dispatchEvent(new CustomEvent('showcaseUpdated'));
|
||||
}
|
||||
|
||||
async function shuffle() {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
container.classList.add('loading');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/random-images?count=20`);
|
||||
if (!response.ok) throw new Error('Failed to fetch');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Clear everything
|
||||
columns.forEach(col => col.innerHTML = '');
|
||||
columnHeights = columnHeights.map(() => 0);
|
||||
|
||||
distributeImages(data.images);
|
||||
|
||||
// Scroll to top
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
} catch (err) {
|
||||
console.error('Shuffle error:', err);
|
||||
} finally {
|
||||
container.classList.remove('loading');
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
if (isLoading) return;
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/random-images?count=${LOAD_BATCH_SIZE}`);
|
||||
if (!response.ok) throw new Error('Failed to fetch');
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.images.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
distributeImages(data.images);
|
||||
} catch (err) {
|
||||
console.error('Load more error:', err);
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatTag(tag) {
|
||||
const escape = (s) => {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = s;
|
||||
return div.innerHTML;
|
||||
};
|
||||
|
||||
const icons = {
|
||||
artist: '🎨',
|
||||
archive: '🗜️',
|
||||
character: '👤',
|
||||
series: '📺',
|
||||
rating: '⚠️'
|
||||
};
|
||||
|
||||
const icon = icons[tag.kind];
|
||||
if (icon) {
|
||||
const label = tag.name.includes(':') ? tag.name.split(':', 2)[1] : tag.name;
|
||||
return `${icon} ${escape(label)}`;
|
||||
} else {
|
||||
return `#${escape(tag.name)}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Start
|
||||
init();
|
||||
})();
|
||||
Reference in New Issue
Block a user