browser / index.html
malcolmrey's picture
Upload 9 files
867460f verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mal's Models</title>
<script type="text/javascript" src="data-civitai.js"></script>
<script type="text/javascript" src="data-huggingface.js"></script>
<script type="text/javascript" src="data-filenames.js"></script>
<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="comparator.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>
/* Theme CSS Variables */
:root {
/* Dark theme (default) */
--bg-gradient-start: #0f172a;
--bg-gradient-end: #1e293b;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--link-color: #60a5fa;
--link-hover: #93c5fd;
--card-bg: rgba(30, 41, 59, 0.7);
--card-bg-solid: #1e293b;
--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.8);
--shadow-color: rgba(0, 0, 0, 0.3);
--accent-blue: #60a5fa;
--accent-purple: #a78bfa;
}
[data-theme="light"] {
--bg-gradient-start: #f8fafc;
--bg-gradient-end: #e2e8f0;
--text-primary: #1e293b;
--text-secondary: #475569;
--text-muted: #64748b;
--link-color: #2563eb;
--link-hover: #1d4ed8;
--card-bg: rgba(255, 255, 255, 0.9);
--card-bg-solid: #ffffff;
--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);
--accent-blue: #2563eb;
--accent-purple: #7c3aed;
}
* {
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;
transition: background 0.3s ease, color 0.3s ease;
}
a {
text-decoration: none;
color: var(--link-color);
transition: color 0.2s;
}
a:hover {
color: var(--link-hover);
}
.header {
max-width: 1400px;
margin: 0 auto 30px;
background: var(--card-bg);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 32px var(--shadow-color);
border: 1px solid var(--card-border);
transition: background 0.3s ease, border-color 0.3s ease;
}
.header-title-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
gap: 16px;
flex-wrap: wrap;
}
.header h1 {
font-size: 28px;
font-weight: 700;
margin: 0;
background: linear-gradient(135deg, var(--accent-blue) 0%, var(--accent-purple) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.profile-links {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
padding: 8px 12px;
background: var(--input-bg);
border-radius: 8px;
transition: background 0.3s ease;
}
.profile-links a {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: rgba(96, 165, 250, 0.1);
border-radius: 6px;
font-size: 14px;
transition: all 0.2s;
}
.profile-links a:hover {
background: rgba(96, 165, 250, 0.2);
transform: translateY(-1px);
}
.last-update {
text-align: center;
font-size: 13px;
color: var(--text-secondary);
margin-bottom: 16px;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
justify-content: center;
}
.search-container {
flex: 1 1 auto;
min-width: 300px;
max-width: 600px;
position: relative;
}
.search-container input[type="text"] {
width: 100%;
padding: 12px 16px;
padding-right: 100px;
background: var(--input-bg);
border: 2px solid var(--input-border);
border-radius: 12px;
color: var(--input-text);
font-size: 15px;
font-family: inherit;
transition: all 0.3s;
}
.search-container input[type="text"]:focus {
outline: none;
border-color: var(--accent-blue);
background: var(--input-bg);
box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.1);
}
.search-container input[type="text"]::placeholder {
color: var(--text-muted);
}
.clear-btn {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
padding: 6px 14px;
background: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 8px;
color: #fca5a5;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.clear-btn:hover {
background: rgba(239, 68, 68, 0.3);
color: #fecaca;
}
.mode-select {
padding: 12px 40px 12px 16px;
background: var(--input-bg);
border: 2px solid var(--input-border);
border-radius: 12px;
color: var(--input-text);
font-size: 15px;
font-family: inherit;
cursor: pointer;
transition: all 0.3s;
font-weight: 500;
height: 48px;
line-height: 20px;
min-width: 150px;
flex-shrink: 0;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 20px;
box-shadow: none;
outline: none;
}
.mode-select::-ms-expand {
display: none;
}
.mode-select:focus {
outline: none;
border-color: var(--accent-blue);
box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.1);
}
.mode-select option {
background: var(--card-bg-solid);
color: var(--input-text);
padding: 16px 20px;
font-size: 15px;
font-weight: 500;
border: none;
outline: none;
}
.mode-select option:hover {
background: linear-gradient(90deg, rgba(96, 165, 250, 0.15) 0%, rgba(96, 165, 250, 0.05) 100%);
color: var(--accent-blue);
font-weight: 600;
}
.mode-select option:checked,
.mode-select option:focus {
background: linear-gradient(90deg, rgba(96, 165, 250, 0.25) 0%, rgba(96, 165, 250, 0.15) 100%);
color: #93c5fd;
font-weight: 600;
box-shadow: none;
outline: none;
}
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: var(--input-bg);
border: 2px solid var(--input-border);
border-radius: 10px;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
font-weight: 500;
user-select: none;
}
.checkbox-label:hover {
background: var(--hover-bg);
border-color: var(--input-border);
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: var(--accent-blue);
}
.checkbox-label input[type="checkbox"]:checked + span {
color: var(--accent-blue);
}
.stats-bar {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
background: var(--input-bg);
border-radius: 10px;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
transition: background 0.3s ease;
}
.stats-bar label {
color: var(--text-secondary);
}
.stats-bar input {
width: 70px;
padding: 6px 10px;
background: var(--card-bg);
border: 1px solid var(--input-border);
border-radius: 6px;
color: var(--accent-blue);
text-align: center;
font-weight: 700;
font-size: 16px;
}
.mainContent {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 20px;
padding: 0 4px;
}
.element {
background: var(--card-bg);
backdrop-filter: blur(10px);
border-radius: 16px;
overflow: hidden;
border: 1px solid var(--card-border);
transition: all 0.3s;
box-shadow: 0 4px 16px var(--shadow-color);
display: flex;
flex-direction: column;
cursor: pointer;
}
.element:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.4);
border-color: rgba(96, 165, 250, 0.3);
}
.model-name-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
background: var(--input-bg);
border-bottom: 1px solid var(--card-border);
gap: 8px;
}
.element .modelName {
font-weight: 600;
font-size: 14px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
color: var(--input-text);
flex: 1;
min-width: 0;
}
.google-search-icon {
font-size: 14px;
text-decoration: none;
opacity: 0.6;
transition: all 0.2s;
cursor: pointer;
flex-shrink: 0;
}
.google-search-icon:hover {
opacity: 1;
transform: scale(1.2);
}
.model-calendar-icon {
font-size: 14px;
cursor: help;
opacity: 0.7;
transition: opacity 0.2s;
flex-shrink: 0;
}
.model-calendar-icon:hover {
opacity: 1;
}
.element .imageContainer {
position: relative;
width: 100%;
padding-top: 137.5%; /* 192/264 aspect ratio */
overflow: hidden;
background: var(--input-bg);
}
.element .imageContainer img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s;
}
.element:hover .imageContainer img {
transform: scale(1.05);
}
.statsBox {
padding: 16px;
background: var(--input-bg);
font-size: 13px;
line-height: 1.8;
flex: 1;
display: none; /* Hidden in main view */
}
.statsBox .stat-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.statsBox .stat-row:last-child {
margin-bottom: 0;
}
.statsBox .stat-label {
font-weight: 600;
color: var(--text-muted);
min-width: 75px;
}
.statsBox .stat-value {
display: flex;
align-items: center;
gap: 6px;
}
/* Modal Styles */
.modal-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--modal-overlay);
backdrop-filter: blur(8px);
z-index: 1000;
align-items: center;
justify-content: center;
padding: 20px;
animation: fadeIn 0.2s ease-out;
}
.modal-overlay.active {
display: flex;
}
.modal-content {
background: linear-gradient(135deg, var(--bg-gradient-end) 0%, var(--bg-gradient-start) 100%);
border-radius: 20px;
max-width: 1100px;
width: 100%;
max-height: 90vh;
overflow: hidden;
position: relative;
border: 2px solid var(--card-border);
box-shadow: 0 20px 60px var(--shadow-color);
animation: slideUp 0.3s ease-out;
display: flex;
flex-direction: column;
transition: background 0.3s ease, border-color 0.3s ease;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
padding: 24px;
border-bottom: 1px solid var(--card-border);
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
background: var(--card-bg);
backdrop-filter: blur(10px);
z-index: 10;
transition: background 0.3s ease, border-color 0.3s ease;
}
.modal-title {
font-size: 24px;
font-weight: 700;
color: var(--text-primary);
margin: 0;
}
.modal-close {
width: 36px;
height: 36px;
border-radius: 10px;
background: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.3);
color: #fca5a5;
font-size: 20px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
font-weight: 700;
}
.modal-close:hover {
background: rgba(239, 68, 68, 0.3);
color: #fecaca;
transform: rotate(90deg);
}
.modal-body {
display: flex;
flex: 1;
overflow: hidden;
gap: 0;
}
.modal-left {
flex: 0 0 45%;
display: flex;
align-items: center;
justify-content: center;
background: var(--input-bg);
padding: 24px;
transition: background 0.3s ease;
}
.modal-image-container {
width: 100%;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
max-height: 100%;
}
.modal-image-container img {
width: 100%;
height: auto;
display: block;
}
.modal-right {
flex: 1;
padding: 24px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
}
.modal-stats {
display: grid;
gap: 12px;
}
.modal-stat-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: var(--input-bg);
border-radius: 12px;
border: 1px solid var(--card-border);
transition: all 0.2s;
}
.modal-stat-row:hover {
background: var(--hover-bg);
border-color: var(--card-border);
}
.modal-stat-label {
font-weight: 600;
font-size: 15px;
color: var(--text-secondary);
display: flex;
align-items: center;
gap: 8px;
}
.modal-stat-value {
display: flex;
align-items: flex-start;
gap: 12px;
flex-wrap: wrap;
}
.modal-hf-links-container {
display: flex;
flex-direction: column;
gap: 6px;
margin-left: 8px;
}
.modal-hf-link {
padding: 6px 12px;
background: rgba(255, 107, 0, 0.15);
border: 1px solid rgba(255, 107, 0, 0.3);
border-radius: 8px;
font-size: 12px;
font-weight: 600;
color: #ff9d5c;
transition: all 0.2s;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.modal-hf-link:hover {
background: rgba(255, 107, 0, 0.25);
border-color: rgba(255, 107, 0, 0.5);
transform: translateY(-1px);
color: #ffb380;
}
.modal-hf-placeholder {
padding: 6px 12px;
background: var(--input-bg);
border: 1px solid var(--card-border);
border-radius: 8px;
font-size: 12px;
font-weight: 600;
color: var(--text-muted);
display: inline-flex;
align-items: center;
gap: 6px;
opacity: 0.5;
}
.hf-logo {
width: 16px;
height: 16px;
display: inline-block;
}
@media (max-width: 900px) {
.modal-body {
flex-direction: column;
}
.modal-left {
flex: 0 0 auto;
max-height: 400px;
}
.modal-right {
flex: 1;
}
}
.status-icon {
font-size: 16px;
display: inline-block;
}
.status-available {
color: #4ade80;
}
.status-unavailable {
color: #f87171;
}
.status-unknown {
color: #fbbf24;
}
.hf-link {
padding: 2px 8px;
background: rgba(96, 165, 250, 0.1);
border: 1px solid rgba(96, 165, 250, 0.2);
border-radius: 6px;
font-size: 11px;
font-weight: 600;
color: var(--accent-blue);
transition: all 0.2s;
display: inline-block;
}
.hf-link:hover {
background: rgba(96, 165, 250, 0.2);
border-color: rgba(96, 165, 250, 0.4);
}
@media (max-width: 768px) {
.controls {
flex-direction: column;
align-items: stretch;
}
.checkbox-group {
justify-content: center;
}
.mainContent {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 16px;
}
}
/* Loading animation */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.element {
animation: fadeIn 0.4s ease-out;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 12px;
}
::-webkit-scrollbar-track {
background: var(--input-bg);
}
::-webkit-scrollbar-thumb {
background: rgba(148, 163, 184, 0.3);
border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(148, 163, 184, 0.5);
}
/* Image viewer with navigation */
.image-viewer {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.modal-image {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
border-radius: 8px;
}
.image-nav-arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: var(--card-bg-solid);
border: 2px solid var(--input-border);
color: var(--accent-blue);
font-size: 48px;
font-weight: bold;
width: 60px;
height: 60px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
z-index: 10;
line-height: 1;
padding: 0;
user-select: none;
}
.image-nav-arrow:hover {
background: rgba(96, 165, 250, 0.3);
border-color: var(--accent-blue);
color: var(--link-hover);
transform: translateY(-50%) scale(1.1);
box-shadow: 0 0 20px rgba(96, 165, 250, 0.5);
}
.image-nav-arrow:active {
transform: translateY(-50%) scale(0.95);
}
.image-nav-prev {
left: 10px;
}
.image-nav-next {
right: 10px;
}
.image-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
padding: 8px 12px;
background: var(--input-bg);
border-radius: 8px;
gap: 12px;
}
.image-counter {
font-size: 14px;
font-weight: 500;
color: var(--text-muted);
}
.framework-label {
font-size: 13px;
font-weight: 600;
padding: 4px 12px;
border-radius: 12px;
background: linear-gradient(135deg, #60a5fa 0%, #a78bfa 100%);
color: #fff;
text-transform: uppercase;
letter-spacing: 0.5px;
box-shadow: 0 2px 8px rgba(96, 165, 250, 0.3);
}
.framework-label[data-framework="WAN"] {
background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%);
}
.framework-label[data-framework="LyCORIS"] {
background: linear-gradient(135deg, #10b981 0%, #14b8a6 100%);
}
.framework-label[data-framework="Lora"] {
background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%);
}
.framework-label[data-framework="Embedding"] {
background: linear-gradient(135deg, #f97316 0%, #fbbf24 100%);
}
.framework-label[data-framework="Qwen"] {
background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%);
}
.framework-label[data-framework="Flux"] {
background: linear-gradient(135deg, #a855f7 0%, #d946ef 100%);
}
.framework-label[data-framework="SDXL"] {
background: linear-gradient(135deg, #84cc16 0%, #22c55e 100%);
}
.framework-label[data-framework="ZImage"] {
background: linear-gradient(135deg, #ec4899 0%, #f97316 100%);
}
/* Pagination Controls */
.pagination-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 24px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.pagination-btn {
padding: 10px 16px;
background: var(--input-bg);
border: 2px solid var(--input-border);
border-radius: 10px;
color: var(--text-primary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.pagination-btn:hover:not(:disabled) {
background: var(--hover-bg);
border-color: var(--accent-blue);
}
.pagination-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.pagination-info {
padding: 10px 20px;
background: var(--input-bg);
border-radius: 10px;
color: var(--text-muted);
font-size: 14px;
font-weight: 500;
}
/* Sorting and Page Size Controls */
.advanced-controls {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
justify-content: center;
margin-top: 12px;
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
}
.control-label {
color: var(--text-muted);
font-size: 13px;
font-weight: 500;
}
.control-select {
padding: 8px 32px 8px 12px;
background: var(--input-bg);
border: 2px solid var(--input-border);
border-radius: 8px;
color: var(--text-primary);
font-size: 13px;
font-family: inherit;
cursor: pointer;
transition: all 0.2s;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 8px center;
background-size: 16px;
}
.control-select:focus {
outline: none;
border-color: var(--accent-blue);
}
.control-select option {
background: var(--card-bg-solid);
color: var(--input-text);
}
.updates-group {
display: flex;
align-items: center;
gap: 12px;
}
.last-update-inline {
font-size: 12px;
color: var(--text-secondary);
padding: 6px 12px;
background: var(--input-bg);\n border-radius: 8px;
border: 1px solid var(--input-border);
white-space: nowrap;
transition: all 0.3s ease;
}
/* Theme Toggle Button */
.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;
transition: all 0.3s ease;
z-index: 1000;
box-shadow: 0 4px 12px var(--shadow-color);
}
.theme-toggle:hover {
transform: scale(1.1);
border-color: var(--accent-blue);
}
.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;
}
.daily-uploads-btn {
padding: 10px 20px;
background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%);
border: none;
border-radius: 10px;
color: #fff;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.daily-uploads-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(245, 158, 11, 0.3);
}
/* Daily Uploads Modal */
.daily-modal-content {
max-width: 900px;
max-height: 80vh;
}
.daily-modal-body {
padding: 24px;
overflow-y: auto;
max-height: calc(80vh - 100px);
}
.daily-date-selector {
margin-bottom: 20px;
}
.daily-date-select {
width: 100%;
padding: 14px 40px 14px 16px;
background: var(--input-bg);
border: 2px solid var(--input-border);
border-radius: 12px;
color: var(--input-text);
font-size: 16px;
font-family: inherit;
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 12px center;
background-size: 20px;
transition: all 0.3s ease;
}
.daily-date-select:focus {
outline: none;
border-color: var(--accent-blue);
}
.daily-date-select option {
background: var(--card-bg-solid);
color: var(--input-text);
}
.daily-uploads-list {
display: grid;
gap: 12px;
}
.daily-upload-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: var(--input-bg);
border-radius: 12px;
border: 1px solid var(--card-border);
transition: all 0.2s;
}
.daily-upload-item:hover {
background: var(--hover-bg);
border-color: var(--card-border);
}
.daily-upload-person {
font-weight: 600;
color: var(--text-primary);
font-size: 15px;
}
.daily-upload-models {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.daily-upload-model {
padding: 4px 10px;
background: rgba(96, 165, 250, 0.2);
border-radius: 6px;
font-size: 12px;
font-weight: 600;
color: var(--accent-blue);
text-transform: uppercase;
text-decoration: none;
cursor: pointer;
transition: all 0.15s ease;
}
.daily-upload-model:hover {
transform: translateY(-2px);
filter: brightness(1.2);
text-decoration: underline;
}
.daily-upload-model[data-type="wan"] {
background: rgba(239, 68, 68, 0.2);
color: #fca5a5;
}
.daily-upload-model[data-type="flux"] {
background: rgba(168, 85, 247, 0.2);
color: #c4b5fd;
}
.daily-upload-model[data-type="sdxl"] {
background: rgba(34, 197, 94, 0.2);
color: #86efac;
}
.daily-upload-model[data-type="zimage"] {
background: rgba(236, 72, 153, 0.2);
color: #f9a8d4;
}
.daily-upload-model[data-type="qwen"] {
background: rgba(6, 182, 212, 0.2);
color: #67e8f9;
}
.daily-upload-model[data-type="lora"] {
background: rgba(59, 130, 246, 0.2);
color: #93c5fd;
}
.daily-upload-model[data-type="locon"] {
background: rgba(16, 185, 129, 0.2);
color: #6ee7b7;
}
.daily-upload-model[data-type="embedding"] {
background: rgba(249, 115, 22, 0.2);
color: #fdba74;
}
.no-uploads-message {
text-align: center;
padding: 40px;
color: var(--text-secondary);
font-size: 15px;
}
.daily-stats {
padding: 16px;
background: var(--input-bg);
border-radius: 10px;
margin-bottom: 16px;
display: flex;
justify-content: center;
gap: 24px;
transition: background 0.3s ease;
}
.daily-stat {
text-align: center;
}
.daily-stat-value {
font-size: 24px;
font-weight: 700;
color: var(--accent-blue);
}
.daily-stat-label {
font-size: 12px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
</style>
</head>
<body>
<!-- Theme Toggle Button -->
<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">
<div class="header-title-row">
<h1>🎨 Mal's Models</h1>
<div class="profile-links">
<a href="https://civitai.com/user/malcolmrey" target="_blank">🎨 CivitAI</a>
<a href="https://huggingface.com/malcolmrey" target="_blank">🤗 HuggingFace</a>
<a href="https://buymeacoffee.com/malcolmrey" target="_blank">☕ BuyMeACoffee</a>
<a href="https://reddit.com/r/malcolmrey" target="_blank">💬 Reddit</a>
</div>
</div>
<div class="controls">
<div class="search-container">
<input
id="search"
type="text"
onkeyup="javascript:searchModels(this.value);"
placeholder="Search models..."
/>
<button
class="clear-btn"
onclick="javascript:clearCurrentSearchValue(); javascript:searchModels(getCurrentSearchValue())"
>
Clear
</button>
</div>
<select
class="mode-select"
id="searchMode"
onchange="javascript:searchModels(getCurrentSearchValue());"
>
<option value="available">Trained</option>
<option value="missing">Not trained</option>
</select>
<div class="stats-bar">
<label>Found:</label>
<input id="found" type="text" readonly />
</div>
<div class="updates-group">
<button class="daily-uploads-btn" onclick="openDailyUploadsModal()">
📋 Recent Updates
</button>
<span id="lastUpdateLabel" class="last-update-inline">Updated: N/A</span>
</div>
</div>
<div class="advanced-controls">
<div class="control-group">
<span class="control-label">Sort by:</span>
<select id="sortBy" class="control-select" onchange="resetToFirstPage(); searchModels(getCurrentSearchValue());">
<option value="name-asc">Name (A-Z)</option>
<option value="name-desc">Name (Z-A)</option>
<option value="date-desc">Update Date (Newest)</option>
<option value="date-asc">Update Date (Oldest)</option>
</select>
</div>
<div class="control-group">
<span class="control-label">Period:</span>
<select id="periodFilter" class="control-select" onchange="resetToFirstPage(); searchModels(getCurrentSearchValue());">
<option value="all">All Time</option>
<option value="today">Today</option>
<option value="week">This Week</option>
<option value="month">This Month</option>
</select>
</div>
<div class="control-group">
<span class="control-label">Per page:</span>
<select id="pageSize" class="control-select" onchange="resetToFirstPage(); searchModels(getCurrentSearchValue());">
<option value="10">10</option>
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
<option value="250">250</option>
<option value="500">500</option>
<option value="1000">1000</option>
<option value="all">All</option>
</select>
</div>
</div>
<div class="controls" style="margin-top: 16px;">
<div class="checkbox-group">
<label class="checkbox-label">
<input
id="selectedLora"
type="checkbox"
onclick="javascript:searchModels(getCurrentSearchValue());"
/>
<span>SD LoRA</span>
</label>
<label class="checkbox-label">
<input
id="selectedLocon"
type="checkbox"
onclick="javascript:searchModels(getCurrentSearchValue());"
/>
<span>SD LoCon</span>
</label>
<label class="checkbox-label">
<input
id="selectedEmbedding"
type="checkbox"
onclick="javascript:searchModels(getCurrentSearchValue());"
/>
<span>SD Embedding</span>
</label>
<label class="checkbox-label">
<input
id="selectedFlux"
type="checkbox"
onclick="javascript:searchModels(getCurrentSearchValue());"
/>
<span>Flux</span>
</label>
<label class="checkbox-label">
<input
id="selectedWan"
type="checkbox"
onclick="javascript:searchModels(getCurrentSearchValue());"
/>
<span>WAN</span>
</label>
<label class="checkbox-label">
<input
id="selectedSdxl"
type="checkbox"
onclick="javascript:searchModels(getCurrentSearchValue());"
/>
<span>SDXL</span>
</label>
<label class="checkbox-label">
<input
id="selectedZimage"
type="checkbox"
onclick="javascript:searchModels(getCurrentSearchValue());"
/>
<span>ZImage</span>
</label>
<label class="checkbox-label">
<input
id="selectedQwen"
type="checkbox"
onclick="javascript:searchModels(getCurrentSearchValue());"
/>
<span>Qwen</span>
</label>
</div>
</div>
</div>
<div id="paginationControls" class="pagination-controls" style="display: none;">
<button id="firstPageBtn" class="pagination-btn" onclick="firstPage()">⇤ First</button>
<button id="prevPageBtn" class="pagination-btn" onclick="previousPage()">← Previous</button>
<span id="paginationInfo" class="pagination-info"></span>
<button id="nextPageBtn" class="pagination-btn" onclick="nextPage()">Next →</button>
<button id="lastPageBtn" class="pagination-btn" onclick="lastPage()">Last ⇥</button>
</div>
<div id="mainContent" class="mainContent"></div>
<div id="paginationControlsBottom" class="pagination-controls" style="display: none;">
<button id="firstPageBtnBottom" class="pagination-btn" onclick="firstPage()">⇤ First</button>
<button id="prevPageBtnBottom" class="pagination-btn" onclick="previousPage()">← Previous</button>
<span id="paginationInfoBottom" class="pagination-info"></span>
<button id="nextPageBtnBottom" class="pagination-btn" onclick="nextPage()">Next →</button>
<button id="lastPageBtnBottom" class="pagination-btn" onclick="lastPage()">Last ⇥</button>
</div>
<!-- Recent Updates Modal -->
<div id="dailyUploadsModal" class="modal-overlay" onclick="closeDailyUploadsModalOnOverlay(event)">
<div class="modal-content daily-modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 class="modal-title">📋 Recent Updates</h2>
<button class="modal-close" onclick="closeDailyUploadsModal()" aria-label="Close">×</button>
</div>
<div class="daily-modal-body">
<div class="daily-date-selector">
<select id="dailyDateSelect" class="daily-date-select" onchange="renderDailyUploads()">
</select>
</div>
<div id="dailyStats" class="daily-stats"></div>
<div id="dailyUploadsList" class="daily-uploads-list"></div>
</div>
</div>
</div>
<!-- Modal -->
<div id="modalOverlay" class="modal-overlay" onclick="closeModalOnOverlay(event)">
<div class="modal-content" onclick="event.stopPropagation()">
<div class="modal-header">
<h2 id="modalTitle" class="modal-title"></h2>
<button class="modal-close" onclick="closeModal()" aria-label="Close">×</button>
</div>
<div class="modal-body">
<div class="modal-left">
<div class="modal-image-container">
<div class="image-viewer">
<button id="prevImageBtn" class="image-nav-arrow image-nav-prev" onclick="previousImage()" aria-label="Previous image" style="display:none;"></button>
<img id="modalImage" src="" alt="" class="modal-image" />
<button id="nextImageBtn" class="image-nav-arrow image-nav-next" onclick="nextImage()" aria-label="Next image" style="display:none;"></button>
</div>
<div id="imageInfo" class="image-info" style="display:none;">
<div id="imageCounter" class="image-counter"></div>
<div id="frameworkLabel" class="framework-label"></div>
</div>
</div>
</div>
<div class="modal-right">
<div id="modalStats" class="modal-stats"></div>
</div>
</div>
</div>
</div>
<script type="text/javascript">
// Theme management
function toggleTheme() {
const body = document.body;
const currentTheme = body.getAttribute('data-theme');
const newTheme = currentTheme === 'light' ? 'dark' : 'light';
if (newTheme === 'dark') {
body.removeAttribute('data-theme');
} else {
body.setAttribute('data-theme', newTheme);
}
// Save preference
localStorage.setItem('theme', newTheme);
}
// Initialize theme from localStorage
function initTheme() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
document.body.setAttribute('data-theme', 'light');
}
}
// Call on page load
initTheme();
// Pagination state
let currentPage = 1;
let totalPages = 1;
let filteredResults = [];
// Upload data (from data-hf-uploaded.js)
const uploadData = {
uploadDates: window.uploadDates || [],
data: window.uploadedData || {}
};
// Update the "Last Updated" label with the latest date
function updateLastUpdateLabel() {
const label = document.getElementById('lastUpdateLabel');
if (label && uploadData.uploadDates.length > 0) {
const d = new Date(uploadData.uploadDates[0]);
const formatted = `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}`;
label.textContent = `Updated: ${formatted}`;
}
}
// Initialize the label
updateLastUpdateLabel();
function getPageSize() {
const value = document.getElementById('pageSize').value;
return value === 'all' ? Infinity : parseInt(value, 10);
}
function getSortBy() {
return document.getElementById('sortBy').value;
}
function getPeriodFilter() {
return document.getElementById('periodFilter').value;
}
function resetToFirstPage() {
currentPage = 1;
}
function getPersonUploadDate(personKey) {
const personData = uploadData.data[personKey];
return personData?.lastUpdated || null;
}
function isWithinPeriod(dateStr, period) {
if (!dateStr || period === 'all') return true;
const date = new Date(dateStr);
const now = new Date();
switch (period) {
case 'today':
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
return date >= todayStart;
case 'week':
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
return date >= weekAgo;
case 'month':
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
return date >= monthAgo;
default:
return true;
}
}
function sortModels(models, sortBy) {
const sorted = [...models];
switch (sortBy) {
case 'name-asc':
sorted.sort((a, b) => a.key.localeCompare(b.key));
break;
case 'name-desc':
sorted.sort((a, b) => b.key.localeCompare(a.key));
break;
case 'date-desc':
sorted.sort((a, b) => {
const dateA = getPersonUploadDate(a.key) || '1970-01-01';
const dateB = getPersonUploadDate(b.key) || '1970-01-01';
return dateB.localeCompare(dateA);
});
break;
case 'date-asc':
sorted.sort((a, b) => {
const dateA = getPersonUploadDate(a.key) || '2100-01-01';
const dateB = getPersonUploadDate(b.key) || '2100-01-01';
return dateA.localeCompare(dateB);
});
break;
}
return sorted;
}
function updatePaginationControls() {
const controls = document.getElementById('paginationControls');
const info = document.getElementById('paginationInfo');
const prevBtn = document.getElementById('prevPageBtn');
const nextBtn = document.getElementById('nextPageBtn');
const controlsBottom = document.getElementById('paginationControlsBottom');
const infoBottom = document.getElementById('paginationInfoBottom');
const prevBtnBottom = document.getElementById('prevPageBtnBottom');
const nextBtnBottom = document.getElementById('nextPageBtnBottom');
const pageSize = getPageSize();
if (pageSize === Infinity || totalPages <= 1) {
controls.style.display = 'none';
controlsBottom.style.display = 'none';
return;
}
controls.style.display = 'flex';
controlsBottom.style.display = 'flex';
const pageText = `Page ${currentPage} of ${totalPages}`;
info.textContent = pageText;
infoBottom.textContent = pageText;
const firstBtn = document.getElementById('firstPageBtn');
const lastBtn = document.getElementById('lastPageBtn');
const firstBtnBottom = document.getElementById('firstPageBtnBottom');
const lastBtnBottom = document.getElementById('lastPageBtnBottom');
const isFirstPage = currentPage <= 1;
const isLastPage = currentPage >= totalPages;
prevBtn.disabled = isFirstPage;
firstBtn.disabled = isFirstPage;
nextBtn.disabled = isLastPage;
lastBtn.disabled = isLastPage;
prevBtnBottom.disabled = isFirstPage;
firstBtnBottom.disabled = isFirstPage;
nextBtnBottom.disabled = isLastPage;
lastBtnBottom.disabled = isLastPage;
}
function firstPage() {
if (currentPage > 1) {
currentPage = 1;
renderCurrentPage();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}
function previousPage() {
if (currentPage > 1) {
currentPage--;
renderCurrentPage();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}
function nextPage() {
if (currentPage < totalPages) {
currentPage++;
renderCurrentPage();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}
function lastPage() {
if (currentPage < totalPages) {
currentPage = totalPages;
renderCurrentPage();
window.scrollTo({ top: 0, behavior: 'smooth' });
}
}
function renderCurrentPage() {
const pageSize = getPageSize();
const contentDiv = document.getElementById('mainContent');
contentDiv.innerHTML = '';
let pageItems;
if (pageSize === Infinity) {
pageItems = filteredResults;
} else {
const start = (currentPage - 1) * pageSize;
const end = start + pageSize;
pageItems = filteredResults.slice(start, end);
}
pageItems.forEach((element) => {
const card = document.createElement('div');
card.className = 'element';
const uploadDate = getPersonUploadDate(element.key);
const googleSearchHtml = `<a href="https://www.google.com/search?q=${encodeURIComponent(element.key)}" target="_blank" class="google-search-icon" title="Search on Google" onclick="event.stopPropagation()">🔍</a>`;
const calendarHtml = uploadDate
? `<span class="model-calendar-icon" title="${getPersonUpdateTooltip(element.key)}">📅</span>`
: '';
card.innerHTML = `
<div class="model-name-row">
<span class="modelName" title="${escapeHtml(element.key)}">${element.key}</span>
${googleSearchHtml}
${calendarHtml}
</div>
<div class="imageContainer">
<img src="${element.imageUrl ?? unknownImage}" alt="${escapeHtml(element.key)}" />
</div>
`;
card.addEventListener('click', () => openModal(element));
contentDiv.appendChild(card);
});
updatePaginationControls();
}
function getPersonUpdateTooltip(personKey) {
const personData = uploadData.data?.[personKey];
if (!personData || !personData.models) {
return 'No update info available';
}
const typeNames = {
locon: 'SD LoCon',
lora: 'SD LoRA',
embedding: 'SD Embedding',
flux: 'Flux',
wan: 'WAN',
sdxl: 'SDXL',
qwen: 'Qwen',
zimage: 'ZImage'
};
// Collect all dates with their model types
const dateEntries = [];
for (const [modelType, modelInfoArray] of Object.entries(personData.models)) {
if (Array.isArray(modelInfoArray) && modelInfoArray.length > 0) {
const mostRecent = modelInfoArray[0];
if (mostRecent?.uploadedAt) {
const dateStr = mostRecent.uploadedAt;
const d = new Date(dateStr);
const displayDate = `${d.getFullYear()}.${String(d.getMonth() + 1).padStart(2, '0')}.${String(d.getDate()).padStart(2, '0')}`;
const displayName = typeNames[modelType] || modelType;
dateEntries.push({ name: displayName, date: displayDate, sortDate: dateStr });
}
}
}
if (dateEntries.length === 0) {
return 'No update info available';
}
// Sort by date descending to find newest
dateEntries.sort((a, b) => b.sortDate.localeCompare(a.sortDate));
const newestDate = dateEntries[0].sortDate;
const lines = ['Update History:'];
// Re-sort alphabetically for display
dateEntries.sort((a, b) => a.name.localeCompare(b.name));
for (const entry of dateEntries) {
// Add ⭐ to newest date, but only if there's more than one entry
const isNewest = entry.sortDate === newestDate && dateEntries.length > 1;
const prefix = isNewest ? '⭐ ' : '';
lines.push(`${prefix}${entry.name}: ${entry.date}`);
}
return lines.join('\n');
}
// Recent Updates Modal Functions
function openDailyUploadsModal() {
const modal = document.getElementById('dailyUploadsModal');
const dateSelect = document.getElementById('dailyDateSelect');
// Populate date dropdown with sorted dates (newest first)
const dates = uploadData.uploadDates || [];
dateSelect.innerHTML = dates.map(date => {
const displayDate = new Date(date).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
return `<option value="${date}">${displayDate}</option>`;
}).join('');
if (dates.length === 0) {
dateSelect.innerHTML = '<option value="">No upload data available</option>';
}
modal.classList.add('active');
document.body.style.overflow = 'hidden';
renderDailyUploads();
}
function closeDailyUploadsModal() {
const modal = document.getElementById('dailyUploadsModal');
modal.classList.remove('active');
document.body.style.overflow = '';
}
function closeDailyUploadsModalOnOverlay(event) {
if (event.target.id === 'dailyUploadsModal') {
closeDailyUploadsModal();
}
}
function renderDailyUploads() {
const dateSelect = document.getElementById('dailyDateSelect');
const listContainer = document.getElementById('dailyUploadsList');
const statsContainer = document.getElementById('dailyStats');
const selectedDate = dateSelect.value;
if (!selectedDate || !uploadData.data) {
listContainer.innerHTML = '<div class="no-uploads-message">No update data available. Run the update scan first.</div>';
statsContainer.innerHTML = '';
return;
}
// Find all people who had uploads on this date
const uploadsOnDate = [];
for (const [personKey, personData] of Object.entries(uploadData.data)) {
const modelsOnDate = [];
const models = personData.models || {};
for (const [modelType, modelInfoArray] of Object.entries(models)) {
if (Array.isArray(modelInfoArray)) {
for (const modelInfo of modelInfoArray) {
if (modelInfo.uploadedAt && modelInfo.uploadedAt.startsWith(selectedDate)) {
modelsOnDate.push(modelType);
break; // Only add each model type once
}
}
}
}
if (modelsOnDate.length > 0) {
uploadsOnDate.push({ person: personKey, models: modelsOnDate });
}
}
// Sort alphabetically
uploadsOnDate.sort((a, b) => a.person.localeCompare(b.person));
// Calculate stats
const totalPeople = uploadsOnDate.length;
const totalModels = uploadsOnDate.reduce((sum, item) => sum + item.models.length, 0);
statsContainer.innerHTML = `
<div class="daily-stat">
<div class="daily-stat-value">${totalPeople}</div>
<div class="daily-stat-label">People</div>
</div>
<div class="daily-stat">
<div class="daily-stat-value">${totalModels}</div>
<div class="daily-stat-label">Models</div>
</div>
`;
if (uploadsOnDate.length === 0) {
listContainer.innerHTML = '<div class="no-uploads-message">No updates found for this date.</div>';
return;
}
listContainer.innerHTML = uploadsOnDate.map(item => `
<div class="daily-upload-item">
<span class="daily-upload-person">${escapeHtml(item.person)}</span>
<div class="daily-upload-models">
${item.models.map(m => {
const link = getModelDownloadLink(item.person, m);
return `<a href="${link}" target="_blank" class="daily-upload-model" data-type="${m.toLowerCase()}" onclick="event.stopPropagation();">${m}</a>`;
}).join('')}
</div>
</div>
`).join('');
}
function getModelDownloadLink(personKey, modelType) {
const folderMap = {
locon: 'lycoris',
lora: 'small-loras',
embedding: 'embeddings',
flux: 'flux',
wan: 'wan',
sdxl: 'sdxl',
qwen: 'qwen',
zimage: 'zimage'
};
const folder = folderMap[modelType.toLowerCase()];
if (!folder) {
return '#';
}
// Get the filename from filenames data
const filenamesData = window.filenames || {};
const personFilenames = filenamesData[personKey];
const filenames = personFilenames?.[modelType.toLowerCase()];
if (!filenames || filenames.length === 0) {
// Fallback to repo page
return `https://huggingface.co/malcolmrey/${folder}`;
}
const filename = filenames[0];
// For WAN models, add wan2.1/ prefix
const filePath = modelType.toLowerCase() === 'wan' ? `wan2.1/${filename}` : filename;
return `https://huggingface.co/malcolmrey/${folder}/resolve/main/${filePath}?download=true`;
}
function yesNo(value) {
if (value === undefined) {
return '<span class="status-icon status-unknown">❓</span>';
}
return value
? '<span class="status-icon status-available">✅</span>'
: '<span class="status-icon status-unavailable">❌</span>';
}
function formatHFLink(link, hasHF) {
if (!link) return '';
return `<a href="${link}" target="_blank" class="hf-link">HF</a>`;
}
function formatModalHFLink(isAvailable, link) {
if (isAvailable && link) {
return `<a href="${link}" target="_blank" class="modal-hf-link">🤗 HuggingFace</a>`;
} else if (isAvailable) {
return `<span class="modal-hf-placeholder">🤗 HuggingFace</span>`;
}
return '';
}
function formatModalHFLinks(isAvailable, folder, filenames, isWan = false) {
if (!isAvailable || !filenames || filenames.length === 0) {
return isAvailable ? `<span class="modal-hf-placeholder">🤗 No files found</span>` : '';
}
// Generate a link for each filename, wrapped in a container for vertical layout
const links = filenames.map((filename, index) => {
// For WAN models, add wan2.1/ prefix to the filename in the path
const filePath = isWan ? `wan2.1/${filename}` : filename;
const link = `https://huggingface.co/malcolmrey/${folder}/resolve/main/${filePath}?download=true`;
const versionLabel = filenames.length > 1 ? ` (v${index + 1})` : '';
return `<a href="${link}" target="_blank" class="modal-hf-link">🤗 HuggingFace${versionLabel}</a>`;
}).join('');
return `<div class="modal-hf-links-container">${links}</div>`;
}
function openModal(element) {
const modal = document.getElementById('modalOverlay');
const modalTitle = document.getElementById('modalTitle');
const modalImage = document.getElementById('modalImage');
const modalStats = document.getElementById('modalStats');
// Generate HuggingFace links using actual filenames from data-filenames.js
const personName = element.key;
const filenamesData = window.filenames || {};
const personFilenames = filenamesData[personName] || {};
modalTitle.textContent = element.key;
modalImage.src = element.imageUrl ?? unknownImage;
modalImage.alt = element.key;
modalStats.innerHTML = `
<div class="modal-stat-row">
<span class="modal-stat-label">${yesNo(element.locon)} SD LoCon</span>
<span class="modal-stat-value">
${formatModalHFLinks(element.locon, 'lycoris', personFilenames.locon)}
</span>
</div>
<div class="modal-stat-row">
<span class="modal-stat-label">${yesNo(element.lora)} SD LoRA</span>
<span class="modal-stat-value">
${formatModalHFLinks(element.lora, 'small-loras', personFilenames.lora)}
</span>
</div>
<div class="modal-stat-row">
<span class="modal-stat-label">${yesNo(element.embedding)} SD Embedding</span>
<span class="modal-stat-value">
${formatModalHFLinks(element.embedding, 'embeddings', personFilenames.embedding)}
</span>
</div>
<div class="modal-stat-row">
<span class="modal-stat-label">${yesNo(element.flux)} Flux</span>
<span class="modal-stat-value">
${formatModalHFLinks(element.flux, 'flux', personFilenames.flux)}
</span>
</div>
<div class="modal-stat-row">
<span class="modal-stat-label">${yesNo(element.wan)} WAN</span>
<span class="modal-stat-value">
${formatModalHFLinks(element.wan, 'wan', personFilenames.wan, true)}
</span>
</div>
<div class="modal-stat-row">
<span class="modal-stat-label">${yesNo(element.sdxl)} SDXL</span>
<span class="modal-stat-value">
${formatModalHFLinks(element.sdxl, 'sdxl', personFilenames.sdxl)}
</span>
</div>
<div class="modal-stat-row">
<span class="modal-stat-label">${yesNo(element.zimage)} ZImage</span>
<span class="modal-stat-value">
${formatModalHFLinks(element.zimage, 'zimage', personFilenames.zimage)}
</span>
</div>
<div class="modal-stat-row">
<span class="modal-stat-label">${yesNo(element.qwen)} Qwen</span>
<span class="modal-stat-value">
${formatModalHFLinks(element.qwen, 'qwen', personFilenames.qwen)}
</span>
</div>
`;
modal.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeModal() {
const modal = document.getElementById('modalOverlay');
modal.classList.remove('active');
document.body.style.overflow = '';
}
function closeModalOnOverlay(event) {
if (event.target.id === 'modalOverlay') {
closeModal();
}
}
// Close modal on ESC key
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeModal();
}
});
const notMatched = {
lycoris: [],
lora: [],
embedding: [],
flux: [],
wan: [],
sdxl: [],
qwen: [],
};
models.lycorises.forEach((lycoris) => {
const key = prepareKey(lycoris.name);
if (presence[key] !== undefined) {
presence[key].loconCivitai = true;
setImageUrl(key, lycoris.imageUrl);
presence[key].loconCivitaiLink = lycoris.url;
} else if (!isKnownSkippableKey(key)) {
notMatched.lycoris.push(key);
}
});
models.loras.forEach((lora) => {
const key = prepareKey(lora.name);
if (presence[key] !== undefined) {
presence[key].loraCivitai = true;
setImageUrl(key, lora.imageUrl);
presence[key].loraCivitaiLink = lora.url;
} else if (!isKnownSkippableKey(key)) {
notMatched.lora.push(key);
}
});
models.embeddings.forEach((embedding) => {
const key = prepareKey(embedding.name);
if (presence[key] !== undefined) {
presence[key].embeddingCivitai = true;
setImageUrl(key, embedding.imageUrl);
presence[key].embeddingCivitaiLink = embedding.url;
} else if (!isKnownSkippableKey(key)) {
notMatched.embedding.push(key);
}
});
models.fluxes.forEach((flux) => {
const key = prepareKey(flux.name);
if (presence[key] !== undefined) {
presence[key].fluxCivitai = true;
setImageUrl(key, flux.imageUrl);
presence[key].fluxCivitaiLink = flux.url;
} else if (!isKnownSkippableKey(key)) {
notMatched.flux.push(key);
}
});
models.wans.forEach((wan) => {
const key = prepareKey(wan.name);
if (presence[key] !== undefined) {
presence[key].wanCivitai = true;
setImageUrl(key, wan.imageUrl);
presence[key].wanCivitaiLink = wan.url;
} else if (!isKnownSkippableKey(key)) {
notMatched.wan.push(key);
}
});
models.sdxls.forEach((sdxl) => {
const key = prepareKey(sdxl.name);
if (presence[key] !== undefined) {
presence[key].sdxlCivitai = true;
setImageUrl(key, sdxl.imageUrl);
presence[key].sdxlCivitaiLink = sdxl.url;
} else if (!isKnownSkippableKey(key)) {
notMatched.sdxl.push(key);
}
});
models.qwens.forEach((qwen) => {
const key = prepareKey(qwen.name);
if (presence[key] !== undefined) {
presence[key].qwenCivitai = true;
setImageUrl(key, qwen.imageUrl);
presence[key].qwenCivitaiLink = qwen.url;
} else if (!isKnownSkippableKey(key)) {
notMatched.qwen.push(key);
}
});
console.log(notMatched);
// Set primary imageUrl based on WAN > Civitai > HF samples priority
for (const key in presence) {
const personImages = buildImagesList(key, presence[key]);
if (personImages.length > 0) {
// Use the first image from our prioritized list (WAN first, then Civitai by type, then others)
presence[key].imageUrl = personImages[0].url;
}
}
const presenceModels = [];
for (const property in presence) {
const element = {
key: property,
locon: presence[property].locon,
lora: presence[property].lora,
embedding: presence[property].embedding,
flux: presence[property].flux,
wan: presence[property].wan,
sdxl: presence[property].sdxl,
zimage: presence[property].zimage,
qwen: presence[property].qwen,
mega: undefined,
imageUrl: presence[property]?.imageUrl ?? undefined,
loconHFLink: presence[property]?.loconHFLink,
loraHFLink: presence[property]?.loraHFLink,
embeddingHFLink: presence[property]?.embeddingHFLink,
fluxHFLink: presence[property]?.fluxHFLink,
wanHFLink: presence[property]?.wanHFLink,
sdxlHFLink: presence[property]?.sdxlHFLink,
zimageHFLink: presence[property]?.zimageHFLink,
qwenHFLink: presence[property]?.qwenHFLink,
};
presenceModels.push(element);
}
function searchModelsModern(value) {
const lowerCaseValue = value.toLowerCase();
const sortBy = getSortBy();
const periodFilter = getPeriodFilter();
const pageSize = getPageSize();
// Filter by search term and type
let filtered = presenceModels.filter((element) => {
return (
(element.key.toLowerCase().includes(lowerCaseValue) || value === '*') &&
filterByType(element)
);
});
// Filter by period
if (periodFilter !== 'all') {
filtered = filtered.filter((element) => {
const uploadDate = getPersonUploadDate(element.key);
return isWithinPeriod(uploadDate, periodFilter);
});
}
// Sort
filtered = sortModels(filtered, sortBy);
// Store for pagination
filteredResults = filtered;
// Calculate total pages
if (pageSize === Infinity) {
totalPages = 1;
} else {
totalPages = Math.ceil(filtered.length / pageSize);
}
// Ensure current page is valid
if (currentPage > totalPages) {
currentPage = Math.max(1, totalPages);
}
document.getElementById('found').value = filtered.length;
renderCurrentPage();
}
// Override the searchModels function
window.searchModels = searchModelsModern;
searchModels(getCurrentSearchValue());
// Image navigation for modal
let currentImages = [];
let currentImageIndex = 0;
let currentPersonKey = '';
function buildImagesList(personKey, personData) {
const images = [];
const hfImages = window.hfImages || {};
const personHFImages = hfImages[personKey] || {};
const civitaiTypMapping = { 'LoCon': 'LyCORIS', 'LORA': 'Lora', 'TextualInversion': 'Embedding' };
// 1. Add ZImage samples first (from HF samples data) - highest priority
if (personHFImages.ZImage) {
images.push(...personHFImages.ZImage);
}
// 2. Add WAN samples second (from HF samples data)
if (personHFImages.WAN) {
images.push(...personHFImages.WAN);
}
// 3. Add Civitai images by type
// Check each framework in personData for Civitai models
const checkCivitaiImage = (framework, civitaiModels) => {
if (!civitaiModels) return;
const civitaiModel = civitaiModels.find(m => {
const normalizedName = m.name.toLowerCase().replaceAll(' ', '').replaceAll('-', '').replaceAll("'", '');
return normalizedName === personKey;
});
if (civitaiModel && civitaiModel.imageUrl) {
const mappedFramework = civitaiTypMapping[civitaiModel.type] || civitaiModel.type;
if (mappedFramework !== 'Civitai') {
images.push({ url: civitaiModel.imageUrl, framework: mappedFramework });
}
}
};
checkCivitaiImage('LyCORIS', models.lycorises);
checkCivitaiImage('Lora', models.loras);
checkCivitaiImage('Embedding', models.embeddings);
checkCivitaiImage('Flux', models.fluxes);
checkCivitaiImage('WAN', models.wans);
checkCivitaiImage('SDXL', models.sdxls);
checkCivitaiImage('Qwen', models.qwens);
// 4. Add other HF samples (excluding ZImage and WAN which are already added)
const frameworkOrder = ['LyCORIS', 'Lora', 'Embedding', 'Flux', 'SDXL', 'Qwen'];
for (const fw of frameworkOrder) {
if (personHFImages[fw]) {
images.push(...personHFImages[fw]);
}
}
return images;
}
function updateImageDisplay() {
const modalImage = document.getElementById('modalImage');
const imageCounter = document.getElementById('imageCounter');
const frameworkLabel = document.getElementById('frameworkLabel');
const imageInfo = document.getElementById('imageInfo');
const prevBtn = document.getElementById('prevImageBtn');
const nextBtn = document.getElementById('nextImageBtn');
if (currentImages.length > 0) {
const currentImage = currentImages[currentImageIndex];
modalImage.src = currentImage.url;
if (currentImages.length > 1) {
imageCounter.textContent = `${currentImageIndex + 1} / ${currentImages.length}`;
imageInfo.style.display = 'flex';
prevBtn.style.display = 'flex';
nextBtn.style.display = 'flex';
} else {
imageCounter.textContent = '';
prevBtn.style.display = 'none';
nextBtn.style.display = 'none';
imageInfo.style.display = 'flex';
}
frameworkLabel.textContent = currentImage.framework;
frameworkLabel.setAttribute('data-framework', currentImage.framework);
}
}
function previousImage() {
if (currentImages.length > 1) {
currentImageIndex = (currentImageIndex - 1 + currentImages.length) % currentImages.length;
updateImageDisplay();
}
}
function nextImage() {
if (currentImages.length > 1) {
currentImageIndex = (currentImageIndex + 1) % currentImages.length;
updateImageDisplay();
}
}
// Update openModal to use image navigation
const originalOpenModal = openModal;
openModal = function(element) {
currentPersonKey = element.key;
currentImages = buildImagesList(element.key, element);
currentImageIndex = 0;
originalOpenModal(element);
if (currentImages.length > 0) {
updateImageDisplay();
}
};
// Keyboard navigation
document.addEventListener('keydown', function(e) {
const modal = document.getElementById('modalOverlay');
const dailyModal = document.getElementById('dailyUploadsModal');
if (e.key === 'Escape') {
if (dailyModal.classList.contains('active')) {
closeDailyUploadsModal();
} else if (modal.classList.contains('active')) {
closeModal();
}
}
if (modal.classList.contains('active')) {
if (e.key === 'ArrowLeft') {
e.preventDefault();
previousImage();
} else if (e.key === 'ArrowRight') {
e.preventDefault();
nextImage();
}
}
});
</script>
</body>
</html>