browser / gallery.html
malcolmrey's picture
Upload gallery.html
7aac07b verified
Raw
History Blame Contribute Delete
35.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Samples Gallery</title>
<script type="text/javascript" src="data-hf-images.js"></script>
<script type="text/javascript" src="data-hf-uploaded.js"></script>
<script type="text/javascript" src="data-filenames.js"></script>
<script type="text/javascript" src="data-thumbnails.js"></script>
<script type="text/javascript" src="data-civitai.js"></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-gradient-start: #0f172a;
--bg-gradient-end: #1e293b;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--link-color: #60a5fa;
--card-bg: rgba(30, 41, 59, 0.7);
--card-border: rgba(148, 163, 184, 0.1);
--input-bg: rgba(15, 23, 42, 0.6);
--input-border: rgba(148, 163, 184, 0.2);
--input-text: #e2e8f0;
--hover-bg: rgba(96, 165, 250, 0.2);
--modal-overlay: rgba(0, 0, 0, 0.85);
--shadow-color: rgba(0, 0, 0, 0.3);
}
[data-theme="light"] {
--bg-gradient-start: #f8fafc;
--bg-gradient-end: #e2e8f0;
--text-primary: #1e293b;
--text-secondary: #475569;
--text-muted: #64748b;
--link-color: #2563eb;
--card-bg: rgba(255, 255, 255, 0.9);
--card-border: rgba(148, 163, 184, 0.3);
--input-bg: rgba(255, 255, 255, 0.8);
--input-border: rgba(148, 163, 184, 0.4);
--input-text: #1e293b;
--hover-bg: rgba(37, 99, 235, 0.1);
--modal-overlay: rgba(0, 0, 0, 0.5);
--shadow-color: rgba(0, 0, 0, 0.1);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
color: var(--text-primary);
min-height: 100vh;
padding: 20px;
}
a { text-decoration: none; color: var(--link-color); }
a:hover { color: #93c5fd; }
.header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.header h1 {
font-size: 1.5rem;
font-weight: 700;
background: linear-gradient(135deg, #60a5fa, #a78bfa);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.back-link {
font-size: 14px;
padding: 6px 12px;
border-radius: 8px;
background: var(--card-bg);
border: 1px solid var(--card-border);
}
.controls {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
margin-bottom: 24px;
padding: 16px;
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 12px;
}
.control-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.control-group label {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
}
select {
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--input-border);
background: var(--input-bg);
color: var(--input-text);
font-size: 14px;
min-width: 160px;
cursor: pointer;
}
#searchInput {
padding: 8px 12px;
border-radius: 8px;
border: 1px solid var(--input-border);
background: var(--input-bg);
color: var(--input-text);
font-size: 14px;
min-width: 180px;
outline: none;
}
#searchInput:focus { border-color: var(--link-color); }
.stats-bar {
display: flex;
gap: 16px;
align-items: center;
margin-bottom: 16px;
font-size: 14px;
color: var(--text-secondary);
}
.stats-bar strong { color: var(--text-primary); }
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.gallery-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.gallery-card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 24px var(--shadow-color);
}
.gallery-card img {
width: 100%;
aspect-ratio: 3/4;
object-fit: cover;
display: block;
}
.gallery-card video {
width: 100%;
aspect-ratio: 3/4;
object-fit: cover;
display: block;
}
.gallery-card-info {
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.gallery-card-name {
font-size: 13px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gallery-card-meta {
font-size: 11px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.gallery-card-badge {
display: inline-block;
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 8px;
text-transform: uppercase;
letter-spacing: 0.3px;
color: #fff;
background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
}
.gallery-card-badge[data-type="wan"] { background: linear-gradient(135deg, #dc2626, #f59e0b); }
.gallery-card-badge[data-type="flux"] { background: linear-gradient(135deg, #7c3aed, #c026d3); }
.gallery-card-badge[data-type="klein9"] { background: linear-gradient(135deg, #8b5cf6, #a855f7); }
.gallery-card-badge[data-type="ernie"] { background: linear-gradient(135deg, #be185d, #f472b6); }
.gallery-card-badge[data-type="ideogram4"] { background: linear-gradient(135deg, #d97706, #fbbf24); }
.gallery-card-badge[data-type="zimage"] { background: linear-gradient(135deg, #e11d48, #fb7185); }
.gallery-card-badge[data-type="zbase"] { background: linear-gradient(135deg, #0f766e, #14b8a6); }
.gallery-card-badge[data-type="ltx"] { background: linear-gradient(135deg, #0369a1, #22d3ee); }
.gallery-card-badge[data-type="sdxl"] { background: linear-gradient(135deg, #65a30d, #84cc16); }
.gallery-card-badge[data-type="locon"] { background: linear-gradient(135deg, #16a34a, #22c55e); }
.gallery-card-badge[data-type="lora"] { background: linear-gradient(135deg, #3b82f6, #6366f1); }
.gallery-card-badge[data-type="embedding"] { background: linear-gradient(135deg, #92400e, #f59e0b); }
.gallery-card-badge[data-type="qwen"] { background: linear-gradient(135deg, #0284c7, #38bdf8); }
.no-results {
text-align: center;
padding: 60px 20px;
color: var(--text-muted);
font-size: 16px;
}
/* Modal */
.modal-overlay {
display: none;
position: fixed;
inset: 0;
background: var(--modal-overlay);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal-overlay.active {
display: flex;
}
.modal-box {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 16px;
max-width: 90vw;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 25px 50px var(--shadow-color);
}
.modal-top {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--card-border);
}
.modal-title {
font-size: 16px;
font-weight: 600;
}
.modal-person-link {
font-size: 13px;
margin-left: 12px;
}
.modal-close-btn {
background: none;
border: none;
color: var(--text-secondary);
font-size: 24px;
cursor: pointer;
padding: 4px 8px;
border-radius: 8px;
}
.modal-close-btn:hover { background: var(--hover-bg); color: var(--text-primary); }
.modal-media-wrap {
position: relative;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
min-height: 400px;
}
.modal-media-wrap img,
.modal-media-wrap video {
max-width: 100%;
max-height: 70vh;
border-radius: 8px;
object-fit: contain;
}
.nav-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.6);
border: none;
color: #fff;
font-size: 28px;
width: 44px;
height: 44px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.nav-btn:hover { background: rgba(0, 0, 0, 0.85); }
.nav-btn.prev { left: 12px; }
.nav-btn.next { right: 12px; }
.modal-bottom {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
border-top: 1px solid var(--card-border);
gap: 12px;
flex-wrap: wrap;
}
.modal-counter {
font-size: 13px;
color: var(--text-secondary);
}
.modal-filename {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 300px;
}
.modal-actions {
display: flex;
gap: 8px;
}
.btn {
padding: 6px 14px;
border-radius: 8px;
border: 1px solid var(--input-border);
background: var(--input-bg);
color: var(--input-text);
font-size: 13px;
cursor: pointer;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 4px;
}
.btn:hover { background: var(--hover-bg); color: var(--text-primary); }
.theme-toggle {
position: fixed;
top: 20px;
right: 20px;
width: 44px;
height: 44px;
border-radius: 50%;
background: var(--card-bg);
border: 2px solid var(--card-border);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
z-index: 1000;
box-shadow: 0 4px 12px var(--shadow-color);
transition: all 0.3s ease;
}
.theme-toggle:hover { transform: scale(1.1); border-color: var(--link-color); }
.theme-toggle .sun-icon { display: none; }
.theme-toggle .moon-icon { display: block; }
[data-theme="light"] .theme-toggle .sun-icon { display: block; }
[data-theme="light"] .theme-toggle .moon-icon { display: none; }
.pagination-bar { margin: 12px 0; }
.pagination-controls {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px;
}
.pagination-info { font-size: 13px; color: var(--text-secondary); }
.pagination-buttons { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; }
.pagination-buttons .btn { min-width: 36px; justify-content: center; }
.pagination-buttons .btn[disabled] { opacity: 0.4; cursor: default; pointer-events: none; }
.pagination-active { background: var(--link-color) !important; color: #fff !important; border-color: var(--link-color) !important; }
.pagination-ellipsis { padding: 0 6px; color: var(--text-muted); }
</style>
</head>
<body>
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle dark/light mode">
<span class="moon-icon">🌙</span>
<span class="sun-icon">☀️</span>
</button>
<div class="header">
<h1>🖼️ Samples Gallery</h1>
<a class="back-link" href="index.html">← Back to Browser</a>
</div>
<div class="controls">
<div class="control-group">
<label>Date</label>
<select id="dateSelect" onchange="currentPage=1;renderGallery()"></select>
</div>
<div class="control-group">
<label>Model Type</label>
<select id="typeSelect" onchange="currentPage=1;renderGallery()">
<option value="all">All Types</option>
<option value="zbase">ZBase</option>
<option value="zimage">ZImage</option>
<option value="klein9">Klein9</option>
<option value="ernie">Ernie</option>
<option value="ideogram4">Ideogram4</option>
<option value="flux">Flux</option>
<option value="wan">WAN</option>
<option value="sdxl">SDXL</option>
<option value="ltx">LTX</option>
<option value="locon">LyCORIS</option>
<option value="lora">Lora</option>
<option value="embedding">Embedding</option>
<option value="qwen">Qwen</option>
</select>
</div>
<div class="control-group">
<label>Search</label>
<input type="text" id="searchInput" placeholder="Filter by name…" oninput="currentPage=1;renderGallery()" />
</div>
<div class="control-group">
<label>Per Page</label>
<select id="pageSizeSelect" onchange="onPageSizeChange()">
<option value="24">24</option>
<option value="48">48</option>
<option value="96">96</option>
<option value="all">All</option>
</select>
</div>
<div class="control-group" style="justify-content:flex-end;">
<label>&nbsp;</label>
<button class="btn" onclick="clearFilters()">✕ Clear</button>
</div>
<div class="control-group" style="justify-content:flex-end;">
<label>&nbsp;</label>
<button class="btn" id="copyLinkBtn" onclick="copyLink()">🔗 Copy Link</button>
</div>
</div>
<div class="stats-bar" id="statsBar"></div>
<div class="pagination-bar" id="paginationTop"></div>
<div class="gallery-grid" id="galleryGrid"></div>
<div class="pagination-bar" id="paginationBottom"></div>
<div class="no-results" id="noResults" style="display:none;">No samples found for this selection.</div>
<!-- Gallery Modal -->
<div class="modal-overlay" id="galleryModal" onclick="if(event.target===this)closeGalleryModal()">
<div class="modal-box">
<div class="modal-top">
<div>
<span class="modal-title" id="modalPersonName"></span>
<a class="modal-person-link" id="modalPersonLink" href="#" target="_blank">open in browser →</a>
</div>
<button class="modal-close-btn" onclick="closeGalleryModal()">×</button>
</div>
<div class="modal-media-wrap">
<button class="nav-btn prev" onclick="navImage(-1)"></button>
<img id="modalImg" src="" alt="" />
<video id="modalVideo" autoplay loop muted style="display:none;"></video>
<button class="nav-btn next" onclick="navImage(1)"></button>
</div>
<div class="modal-bottom">
<span class="modal-counter" id="modalCounter"></span>
<span class="modal-filename" id="modalFilename"></span>
<div class="modal-actions">
<button class="btn" id="modalDownloadSample" onclick="downloadCurrentSample()">🖼️ Download Sample</button>
<a class="btn" id="modalDownload" href="#" target="_blank">⬇️ Download Model</a>
</div>
</div>
</div>
</div>
<script>
// Theme management
function toggleTheme() {
const currentTheme = document.body.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
if (newTheme === 'dark') {
document.body.removeAttribute('data-theme');
} else {
document.body.setAttribute('data-theme', newTheme);
}
localStorage.setItem('theme', newTheme);
}
// Restore saved theme
(function() {
const saved = localStorage.getItem('theme');
if (saved === 'light') document.body.setAttribute('data-theme', 'light');
})();
// Data references (use window.X to avoid redeclaration of globals from data-*.js)
const _hfImages = window.hfImages || {};
const _uploadData = window.uploadedData || {};
const _dates = window.uploadDates || [];
const _filenamesData = window.filenames || {};
const _thumbnailsData = window.thumbnails || {};
const FOLDER_MAP = {
locon: 'lycoris',
lora: 'small-loras',
embedding: 'embeddings',
flux: 'flux',
wan: 'wan',
sdxl: 'sdxl',
ltx: 'ltx',
qwen: 'qwen',
zimage: 'zimage',
zbase: 'zbase',
klein9: 'klein9',
ernie: 'ernie',
ideogram4: 'ideogram',
};
const FRAMEWORK_TO_TYPE = {
'ZBase': 'zbase',
'ZImage': 'zimage',
'Klein9': 'klein9',
'Ernie': 'ernie',
'Ideogram4': 'ideogram4',
'WAN': 'wan',
'LTX': 'ltx',
'Flux': 'flux',
'SDXL': 'sdxl',
'LyCORIS': 'locon',
'Lora': 'lora',
'Embedding': 'embedding',
'Qwen': 'qwen',
};
// Gallery state
let galleryItems = [];
let allFilteredItems = [];
let modalItems = [];
let modalIndex = 0;
let currentPage = 1;
let pageSize = 24;
// Initialize
function init() {
const dateSelect = document.getElementById('dateSelect');
const dateOptions = _dates.map(d => {
const display = new Date(d).toLocaleDateString('en-US', {
weekday: 'short', year: 'numeric', month: 'short', day: 'numeric'
});
return `<option value="${d}">${display}</option>`;
}).join('');
dateSelect.innerHTML = `<option value="all">All Dates</option>${dateOptions}`;
// Restore URL params
const params = new URLSearchParams(window.location.search);
const paramDate = params.get('date');
const paramType = params.get('type');
const paramSearch = params.get('q');
const paramPageSize = params.get('size');
const paramPage = params.get('page');
if (paramDate === 'all' || (paramDate && _dates.includes(paramDate))) {
dateSelect.value = paramDate;
}
if (paramType) {
document.getElementById('typeSelect').value = paramType;
}
if (paramSearch) {
document.getElementById('searchInput').value = paramSearch;
}
if (paramPageSize) {
const sizeSelect = document.getElementById('pageSizeSelect');
sizeSelect.value = paramPageSize;
pageSize = paramPageSize === 'all' ? 'all' : parseInt(paramPageSize, 10);
}
if (paramPage) {
currentPage = parseInt(paramPage, 10) || 1;
}
renderGallery();
}
function getUploadsForDate(date) {
const results = [];
const seen = new Set();
for (const [personKey, personData] of Object.entries(_uploadData)) {
if (!personData.models) continue;
for (const [modelType, models] of Object.entries(personData.models)) {
for (const model of models) {
const uploadDate = model.uploadedAt.split('T')[0];
if (uploadDate === date) {
// Deduplicate: one card per person+modelType
const dedupeKey = `${personKey}|${modelType}`;
if (seen.has(dedupeKey)) continue;
seen.add(dedupeKey);
results.push({
person: personKey,
modelType,
filename: model.filename,
uploadedAt: model.uploadedAt,
});
}
}
}
}
return results;
}
// Civitai type mapping
const CIVITAI_TYPE_MAP = { 'LoCon': 'LyCORIS', 'LORA': 'Lora', 'TextualInversion': 'Embedding' };
const CIVITAI_COLLECTIONS = {
locon: 'lycorises', lora: 'loras', embedding: 'embeddings',
flux: 'fluxes', wan: 'wans', sdxl: 'sdxls', ltx: 'ltxes', qwen: 'qwens',
};
function getCivitaiImage(personKey, modelType) {
const collection = CIVITAI_COLLECTIONS[modelType];
if (!collection || !models || !models[collection]) return null;
const civitaiModel = models[collection].find(m => {
const normalized = m.name.toLowerCase().replaceAll(' ', '').replaceAll('-', '').replaceAll("'", '');
return normalized === personKey;
});
if (civitaiModel && civitaiModel.imageUrl) {
const fw = CIVITAI_TYPE_MAP[civitaiModel.type] || civitaiModel.type;
return {
url: civitaiModel.imageUrl,
filename: civitaiModel.imageUrl.split('/').pop() || 'civitai-sample',
framework: fw,
};
}
return null;
}
function getSampleImage(personKey, modelType) {
const personImages = _hfImages[personKey] || {};
// Map modelType to framework name
const frameworkNames = {
locon: 'LyCORIS', lora: 'Lora', embedding: 'Embedding',
flux: 'Flux', wan: 'WAN', sdxl: 'SDXL', ltx: 'LTX',
qwen: 'Qwen', zimage: 'ZImage', zbase: 'ZBase',
klein9: 'Klein9', ernie: 'Ernie', ideogram4: 'Ideogram4',
};
const fw = frameworkNames[modelType];
if (fw && personImages[fw] && personImages[fw].length > 0) {
return personImages[fw][0];
}
// Fallback to Civitai
const civitaiImg = getCivitaiImage(personKey, modelType);
if (civitaiImg) return civitaiImg;
// Fallback to thumbnail
if (_thumbnailsData[personKey]) {
return {
url: `https://huggingface.co/datasets/malcolmrey/samples/resolve/main/thumbnails/${encodeURIComponent(personKey)}.jpg`,
filename: `${personKey}.jpg`,
framework: 'Thumbnail',
};
}
return null;
}
function getAllSampleImages(personKey, modelType) {
const personImages = _hfImages[personKey] || {};
const frameworkNames = {
locon: 'LyCORIS', lora: 'Lora', embedding: 'Embedding',
flux: 'Flux', wan: 'WAN', sdxl: 'SDXL', ltx: 'LTX',
qwen: 'Qwen', zimage: 'ZImage', zbase: 'ZBase',
klein9: 'Klein9', ernie: 'Ernie', ideogram4: 'Ideogram4',
};
const fw = frameworkNames[modelType];
const images = [];
if (fw && personImages[fw] && personImages[fw].length > 0) {
images.push(...personImages[fw]);
}
// Add Civitai image if not already covered by HF
const civitaiImg = getCivitaiImage(personKey, modelType);
if (civitaiImg && !images.some(img => img.url === civitaiImg.url)) {
images.push(civitaiImg);
}
return images;
}
function getDownloadUrl(personKey, modelType) {
const folder = FOLDER_MAP[modelType];
if (!folder) return '#';
const personFilenames = _filenamesData[personKey];
const files = personFilenames?.[modelType];
if (!files || files.length === 0) {
return `https://huggingface.co/malcolmrey/${folder}`;
}
const filename = files[0];
const filePath = modelType === 'wan' ? `wan2.1/${filename}` : filename;
return `https://huggingface.co/malcolmrey/${folder}/resolve/main/${filePath}?download=true`;
}
function syncUrlParams(date, type, search) {
const params = new URLSearchParams();
if (date && date !== 'all') params.set('date', date);
if (date === 'all') params.set('date', 'all');
if (type && type !== 'all') params.set('type', type);
if (search) params.set('q', search);
if (pageSize !== 24 && pageSize !== 'all') params.set('size', String(pageSize));
if (pageSize === 'all') params.set('size', 'all');
if (currentPage > 1) params.set('page', String(currentPage));
const qs = params.toString();
const newUrl = window.location.pathname + (qs ? '?' + qs : '');
history.replaceState(null, '', newUrl);
}
function renderGallery() {
const date = document.getElementById('dateSelect').value;
const typeFilter = document.getElementById('typeSelect').value;
if (!date) {
document.getElementById('galleryGrid').innerHTML = '';
document.getElementById('noResults').style.display = 'block';
document.getElementById('statsBar').innerHTML = '';
return;
}
const uploads = date === 'all'
? _dates.flatMap(d => getUploadsForDate(d))
: getUploadsForDate(date);
// Deduplicate: one card per person+modelType (driven by samples, not models)
const seenKeys = new Set();
const deduped = [];
for (const u of uploads) {
const key = `${u.person}|${u.modelType}`;
if (seenKeys.has(key)) continue;
seenKeys.add(key);
deduped.push(u);
}
// Filter by type
let filtered = typeFilter === 'all'
? deduped
: deduped.filter(u => u.modelType === typeFilter);
// Filter by search term
const searchTerm = document.getElementById('searchInput').value.trim().toLowerCase();
if (searchTerm) {
filtered = filtered.filter(u => u.person.toLowerCase().includes(searchTerm));
}
// Skip entries without sample images (only keep those with actual samples or thumbnails)
filtered = filtered.filter(u => getSampleImage(u.person, u.modelType) !== null);
// Sort: by model type, then person name
filtered.sort((a, b) => {
if (a.modelType !== b.modelType) return a.modelType.localeCompare(b.modelType);
return a.person.localeCompare(b.person);
});
allFilteredItems = filtered;
// Sync state to URL
syncUrlParams(date, typeFilter, searchTerm);
const grid = document.getElementById('galleryGrid');
const noResults = document.getElementById('noResults');
const statsBar = document.getElementById('statsBar');
if (filtered.length === 0) {
grid.innerHTML = '';
noResults.style.display = 'block';
statsBar.innerHTML = '';
document.getElementById('paginationTop').innerHTML = '';
document.getElementById('paginationBottom').innerHTML = '';
return;
}
noResults.style.display = 'none';
// Pagination
const totalItems = filtered.length;
const totalPages = pageSize === 'all' ? 1 : Math.ceil(totalItems / pageSize);
if (currentPage > totalPages) currentPage = totalPages;
const startIdx = pageSize === 'all' ? 0 : (currentPage - 1) * pageSize;
const endIdx = pageSize === 'all' ? totalItems : Math.min(startIdx + pageSize, totalItems);
const pageItems = filtered.slice(startIdx, endIdx);
galleryItems = pageItems;
// Stats
const typeCount = {};
filtered.forEach(u => { typeCount[u.modelType] = (typeCount[u.modelType] || 0) + 1; });
const typeSummary = Object.entries(typeCount).map(([t, c]) => `${t}: <strong>${c}</strong>`).join(' · ');
statsBar.innerHTML = `<span>Total: <strong>${filtered.length}</strong> samples</span><span>${typeSummary}</span>`;
// Pagination controls
const paginationHtml = totalPages > 1 ? buildPaginationHtml(currentPage, totalPages, startIdx + 1, endIdx, totalItems) : '';
document.getElementById('paginationTop').innerHTML = paginationHtml;
document.getElementById('paginationBottom').innerHTML = paginationHtml;
// Render cards
grid.innerHTML = pageItems.map((item, idx) => {
const sample = getSampleImage(item.person, item.modelType);
const imgUrl = sample?.url || '';
const isVideo = imgUrl.toLowerCase().endsWith('.mp4');
const mediaHtml = isVideo
? `<video src="${imgUrl}" muted loop autoplay playsinline></video>`
: `<img src="${imgUrl}" alt="${item.person}" loading="lazy" onerror="this.src='unknown.jpg'" />`;
return `
<div class="gallery-card" onclick="openGalleryModal(${idx})">
${mediaHtml}
<div class="gallery-card-info">
<span class="gallery-card-name">${item.person}</span>
<span class="gallery-card-badge" data-type="${item.modelType}">${item.modelType}</span>
</div>
</div>
`;
}).join('');
}
// Pagination helpers
function buildPaginationHtml(page, totalPages, from, to, total) {
let buttons = '';
const prevDisabled = page <= 1 ? 'disabled' : '';
const nextDisabled = page >= totalPages ? 'disabled' : '';
buttons += `<button class="btn" onclick="goToPage(${page - 1})" ${prevDisabled}>← Prev</button>`;
// Page numbers
const range = getPageRange(page, totalPages);
for (const p of range) {
if (p === '...') {
buttons += `<span class="pagination-ellipsis">…</span>`;
} else {
const active = p === page ? 'pagination-active' : '';
buttons += `<button class="btn ${active}" onclick="goToPage(${p})">${p}</button>`;
}
}
buttons += `<button class="btn" onclick="goToPage(${page + 1})" ${nextDisabled}>Next →</button>`;
return `<div class="pagination-controls"><span class="pagination-info">Showing ${from}${to} of ${total}</span><div class="pagination-buttons">${buttons}</div></div>`;
}
function getPageRange(current, total) {
if (total <= 7) return Array.from({length: total}, (_, i) => i + 1);
const pages = [];
pages.push(1);
if (current > 3) pages.push('...');
for (let i = Math.max(2, current - 1); i <= Math.min(total - 1, current + 1); i++) {
pages.push(i);
}
if (current < total - 2) pages.push('...');
pages.push(total);
return pages;
}
function goToPage(page) {
const totalPages = pageSize === 'all' ? 1 : Math.ceil(allFilteredItems.length / pageSize);
if (page < 1 || page > totalPages) return;
currentPage = page;
renderGallery();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
function onPageSizeChange() {
const val = document.getElementById('pageSizeSelect').value;
pageSize = val === 'all' ? 'all' : parseInt(val, 10);
currentPage = 1;
renderGallery();
}
function clearFilters() {
document.getElementById('dateSelect').value = 'all';
document.getElementById('typeSelect').value = 'all';
document.getElementById('searchInput').value = '';
document.getElementById('pageSizeSelect').value = '24';
pageSize = 24;
currentPage = 1;
renderGallery();
}
function copyLink() {
const url = window.location.href;
navigator.clipboard.writeText(url).then(() => {
const btn = document.getElementById('copyLinkBtn');
btn.textContent = '✓ Copied!';
setTimeout(() => { btn.textContent = '🔗 Copy Link'; }, 2000);
});
}
function downloadCurrentSample() {
if (modalItems.length === 0) return;
const current = modalItems[modalIndex];
if (!current || !current.url) return;
const a = document.createElement('a');
a.href = current.url;
a.download = current.filename || current.url.split('/').pop() || 'sample';
a.target = '_blank';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// Modal
function openGalleryModal(idx) {
const item = galleryItems[idx];
if (!item) return;
// Build images list for this person+type
modalItems = getAllSampleImages(item.person, item.modelType);
if (modalItems.length === 0) {
// Show the card image if no dedicated samples
const sample = getSampleImage(item.person, item.modelType);
if (sample) modalItems = [sample];
}
modalIndex = 0;
document.getElementById('modalPersonName').textContent = item.person;
const personLink = document.getElementById('modalPersonLink');
personLink.href = `index.html?personcode=${encodeURIComponent(item.person)}`;
const downloadLink = getDownloadUrl(item.person, item.modelType);
document.getElementById('modalDownload').href = downloadLink;
document.getElementById('galleryModal').classList.add('active');
document.body.style.overflow = 'hidden';
updateModal();
}
function closeGalleryModal() {
document.getElementById('galleryModal').classList.remove('active');
document.body.style.overflow = '';
const video = document.getElementById('modalVideo');
video.pause();
video.removeAttribute('src');
video.load();
}
function navImage(dir) {
if (modalItems.length <= 1) return;
modalIndex = (modalIndex + dir + modalItems.length) % modalItems.length;
updateModal();
}
function updateModal() {
const img = document.getElementById('modalImg');
const video = document.getElementById('modalVideo');
const counter = document.getElementById('modalCounter');
const filenameEl = document.getElementById('modalFilename');
if (modalItems.length === 0) return;
const current = modalItems[modalIndex];
const isVideo = current.url.toLowerCase().endsWith('.mp4');
if (isVideo) {
video.src = current.url;
video.style.display = 'block';
img.style.display = 'none';
} else {
img.src = current.url;
img.alt = current.filename || '';
img.style.display = 'block';
video.style.display = 'none';
video.pause();
video.removeAttribute('src');
video.load();
}
counter.textContent = modalItems.length > 1 ? `${modalIndex + 1} / ${modalItems.length}` : '';
filenameEl.textContent = current.filename || '';
// Show/hide nav buttons
const showNav = modalItems.length > 1;
document.querySelectorAll('.nav-btn').forEach(b => b.style.display = showNav ? 'flex' : 'none');
}
// Keyboard
document.addEventListener('keydown', (e) => {
const modal = document.getElementById('galleryModal');
if (!modal.classList.contains('active')) return;
if (e.key === 'Escape') closeGalleryModal();
if (e.key === 'ArrowLeft') navImage(-1);
if (e.key === 'ArrowRight') navImage(1);
});
init();
</script>
</body>
</html>