Spaces:
Running
Running
| <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) ; color: #fff ; border-color: var(--link-color) ; } | |
| .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> </label> | |
| <button class="btn" onclick="clearFilters()">✕ Clear</button> | |
| </div> | |
| <div class="control-group" style="justify-content:flex-end;"> | |
| <label> </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> | |