| <!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> |
| |
| :root { |
| |
| --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, |
| .copy-link-icon { |
| font-size: 14px; |
| text-decoration: none; |
| opacity: 0.6; |
| transition: all 0.2s; |
| cursor: pointer; |
| flex-shrink: 0; |
| } |
| |
| .google-search-icon:hover, |
| .copy-link-icon:hover { |
| opacity: 1; |
| transform: scale(1.2); |
| } |
| |
| .copy-link-icon.copied { |
| opacity: 1; |
| color: #4caf50; |
| } |
| |
| .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%; |
| 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; |
| } |
| |
| .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-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; |
| } |
| } |
| |
| |
| @keyframes fadeIn { |
| from { |
| opacity: 0; |
| transform: translateY(20px); |
| } |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| .element { |
| animation: fadeIn 0.4s ease-out; |
| } |
| |
| |
| ::-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 { |
| 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="Klein9"] { |
| background: linear-gradient(135deg, #0ea5e9 0%, #facc15 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%, #fb7185 100%); |
| } |
| |
| .framework-label[data-framework="ZBase"] { |
| background: linear-gradient(135deg, #8b5cf6 0%, #22c55e 100%); |
| } |
| |
| |
| .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; |
| } |
| |
| |
| .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); |
| border-radius: 8px; |
| border: 1px solid var(--input-border); |
| white-space: nowrap; |
| transition: all 0.3s ease; |
| } |
| |
| |
| .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-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-person-row { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| } |
| |
| .person-modal-link { |
| font-size: 14px; |
| opacity: 0.6; |
| transition: all 0.2s; |
| cursor: pointer; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| text-decoration: none; |
| } |
| |
| .person-modal-link:hover { |
| opacity: 1; |
| transform: scale(1.2); |
| } |
| |
| .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="klein9"] { |
| background: rgba(234, 179, 8, 0.22); |
| color: #fde047; |
| } |
| |
| .daily-upload-model[data-type="sdxl"] { |
| background: rgba(132, 204, 22, 0.2); |
| color: #bef264; |
| } |
| |
| .daily-upload-model[data-type="zimage"] { |
| background: rgba(236, 72, 153, 0.2); |
| color: #f9a8d4; |
| } |
| |
| .daily-upload-model[data-type="zbase"] { |
| background: rgba(139, 92, 246, 0.2); |
| color: #c4b5fd; |
| } |
| |
| .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(20, 184, 166, 0.2); |
| color: #5eead4; |
| } |
| |
| .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> |
| |
| <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> |
| <option value="daily-desc">Daily (Newest)</option> |
| <option value="daily-asc">Daily (Oldest)</option> |
| </select> |
| </div> |
|
|
| |
| <div class="control-group"> |
| <span class="control-label">From:</span> |
| <select id="dateFrom" class="control-select" onchange="resetToFirstPage(); searchModels(getCurrentSearchValue());"> |
| <option value="">One day</option> |
| </select> |
| </div> |
|
|
| <div class="control-group"> |
| <span class="control-label">To:</span> |
| <select id="dateTo" class="control-select" onchange="resetToFirstPage(); searchModels(getCurrentSearchValue());"> |
| <option value="">One day</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="selectedLtx" |
| type="checkbox" |
| onclick="javascript:searchModels(getCurrentSearchValue());" |
| /> |
| <span>LTX</span> |
| </label> |
|
|
| <label class="checkbox-label"> |
| <input |
| id="selectedZimage" |
| type="checkbox" |
| onclick="javascript:searchModels(getCurrentSearchValue());" |
| /> |
| <span>Z Image Turbo</span> |
| </label> |
|
|
| <label class="checkbox-label"> |
| <input |
| id="selectedZbase" |
| type="checkbox" |
| onclick="javascript:searchModels(getCurrentSearchValue());" |
| /> |
| <span>Z Image Base</span> |
| </label> |
|
|
| <label class="checkbox-label"> |
| <input |
| id="selectedKlein9" |
| type="checkbox" |
| onclick="javascript:searchModels(getCurrentSearchValue());" |
| /> |
| <span>Klein9</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> |
|
|
| |
| <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> |
|
|
| |
| <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" /> |
| <video id="modalVideo" class="modal-image" style="display:none;" controls muted playsinline preload="metadata"></video> |
| <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"> |
| |
| 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); |
| } |
| |
| |
| localStorage.setItem('theme', newTheme); |
| } |
| |
| |
| function initTheme() { |
| const savedTheme = localStorage.getItem('theme'); |
| if (savedTheme === 'light') { |
| document.body.setAttribute('data-theme', 'light'); |
| } |
| } |
| |
| |
| initTheme(); |
| |
| |
| let currentPage = 1; |
| let totalPages = 1; |
| let filteredResults = []; |
| |
| |
| const uploadData = { |
| uploadDates: window.uploadDates || [], |
| data: window.uploadedData || {} |
| }; |
| |
| |
| function getAvailableUploadDates() { |
| const dates = uploadData.uploadDates || []; |
| |
| const uniqueDates = [...new Set(dates)].sort((a, b) => b.localeCompare(a)); |
| return uniqueDates; |
| } |
| |
| |
| function populateDateSelectors() { |
| const fromSelect = document.getElementById('dateFrom'); |
| const toSelect = document.getElementById('dateTo'); |
| const dates = getAvailableUploadDates(); |
| |
| |
| const fromOptions = fromSelect.querySelectorAll('option:not([value=""])'); |
| const toOptions = toSelect.querySelectorAll('option:not([value=""])'); |
| fromOptions.forEach(opt => opt.remove()); |
| toOptions.forEach(opt => opt.remove()); |
| |
| |
| dates.forEach(date => { |
| const formattedDate = new Date(date).toLocaleDateString('en-US', { |
| year: 'numeric', |
| month: 'short', |
| day: 'numeric' |
| }); |
| |
| const optionFrom = document.createElement('option'); |
| optionFrom.value = date; |
| optionFrom.textContent = formattedDate; |
| |
| const optionTo = document.createElement('option'); |
| optionTo.value = date; |
| optionTo.textContent = formattedDate; |
| |
| fromSelect.appendChild(optionFrom.cloneNode(true)); |
| toSelect.appendChild(optionTo.cloneNode(true)); |
| }); |
| } |
| |
| |
| function getDateRange() { |
| const fromValue = document.getElementById('dateFrom').value; |
| const toValue = document.getElementById('dateTo').value; |
| |
| |
| if (!fromValue && !toValue) { |
| return null; |
| } |
| |
| |
| if (!fromValue && toValue) { |
| return { from: toValue, to: toValue }; |
| } |
| if (fromValue && !toValue) { |
| return { from: fromValue, to: fromValue }; |
| } |
| |
| |
| let fromDate = fromValue; |
| let toDate = toValue; |
| |
| if (fromDate > toDate) { |
| |
| [fromDate, toDate] = [toDate, fromDate]; |
| |
| |
| setTimeout(() => { |
| document.getElementById('dateFrom').value = fromDate; |
| document.getElementById('dateTo').value = toDate; |
| }, 0); |
| } |
| |
| return { from: fromDate, to: toDate }; |
| } |
| |
| |
| function isWithinDateRange(dateStr, dateRange) { |
| if (!dateRange || !dateStr) return true; |
| |
| |
| const d = dateStr.split('T')[0]; |
| const from = dateRange.from; |
| const to = dateRange.to; |
| |
| return d >= from && d <= to; |
| } |
| |
| |
| 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}`; |
| } |
| } |
| |
| |
| updateLastUpdateLabel(); |
| populateDateSelectors(); |
| |
| function getPageSize() { |
| const value = document.getElementById('pageSize').value; |
| return value === 'all' ? Infinity : parseInt(value, 10); |
| } |
| |
| function getSortBy() { |
| return document.getElementById('sortBy').value; |
| } |
| |
| |
| |
| |
| |
| function resetToFirstPage() { |
| currentPage = 1; |
| } |
| |
| function getPersonUploadDate(personKey, selectedTypes = null, dateOnly = false) { |
| const personData = uploadData.data[personKey]; |
| if (!personData || !personData.models) return null; |
| |
| |
| if (!selectedTypes) { |
| const dateStr = personData.lastUpdated; |
| return dateOnly && dateStr ? dateStr.split('T')[0] : dateStr; |
| } |
| |
| |
| let mostRecentDate = null; |
| |
| for (const modelType of selectedTypes) { |
| const models = personData.models[modelType]; |
| if (models && models.length > 0) { |
| |
| const latestModel = models[0]; |
| if (latestModel.uploadedAt) { |
| const modelDate = latestModel.uploadedAt; |
| if (!mostRecentDate || modelDate > mostRecentDate) { |
| mostRecentDate = modelDate; |
| } |
| } |
| } |
| } |
| |
| return dateOnly && mostRecentDate ? mostRecentDate.split('T')[0] : mostRecentDate; |
| } |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function sortModels(models, sortBy, selectedTypes = null) { |
| 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, selectedTypes) || '1970-01-01'; |
| const dateB = getPersonUploadDate(b.key, selectedTypes) || '1970-01-01'; |
| return dateB.localeCompare(dateA); |
| }); |
| break; |
| case 'date-asc': |
| sorted.sort((a, b) => { |
| const dateA = getPersonUploadDate(a.key, selectedTypes) || '2100-01-01'; |
| const dateB = getPersonUploadDate(b.key, selectedTypes) || '2100-01-01'; |
| return dateA.localeCompare(dateB); |
| }); |
| break; |
| case 'daily-desc': |
| sorted.sort((a, b) => { |
| |
| const dateA = getPersonUploadDate(a.key, selectedTypes, true) || '1970-01-01'; |
| const dateB = getPersonUploadDate(b.key, selectedTypes, true) || '1970-01-01'; |
| |
| |
| if (dateB !== dateA) { |
| return dateB.localeCompare(dateA); |
| } |
| |
| |
| return a.key.localeCompare(b.key); |
| }); |
| break; |
| case 'daily-asc': |
| sorted.sort((a, b) => { |
| |
| const dateA = getPersonUploadDate(a.key, selectedTypes, true) || '2100-01-01'; |
| const dateB = getPersonUploadDate(b.key, selectedTypes, true) || '2100-01-01'; |
| |
| |
| if (dateA !== dateB) { |
| return dateA.localeCompare(dateB); |
| } |
| |
| |
| return a.key.localeCompare(b.key); |
| }); |
| 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 sortBy = getSortBy(); |
| 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); |
| } |
| |
| |
| const selectedTypes = getSelectedTypes(); |
| const showAllTypes = selectedTypes.length === 0; |
| |
| let lastDate = null; |
| |
| pageItems.forEach((element) => { |
| |
| const dateStr = showAllTypes |
| ? getPersonUploadDate(element.key, null, true) |
| : getPersonUploadDate(element.key, selectedTypes, true); |
| |
| |
| if (sortBy.startsWith('daily-') && dateStr && dateStr !== lastDate) { |
| const formattedDate = formatDateForDisplay(dateStr); |
| const dateHeader = document.createElement('div'); |
| dateHeader.className = 'date-header'; |
| dateHeader.style.cssText = ` |
| grid-column: 1 / -1; |
| margin: 20px 0 10px; |
| padding: 12px 16px; |
| background: var(--card-bg); |
| border-radius: 12px; |
| border-left: 4px solid var(--accent-blue); |
| font-weight: 600; |
| color: var(--accent-blue); |
| font-size: 16px; |
| box-shadow: 0 2px 8px var(--shadow-color); |
| `; |
| dateHeader.innerHTML = `📅 ${formattedDate}`; |
| contentDiv.appendChild(dateHeader); |
| lastDate = dateStr; |
| } |
| const card = document.createElement('div'); |
| card.className = 'element'; |
| |
| const uploadDate = getPersonUploadDate(element.key); |
| const copyLinkHtml = `<span class="copy-link-icon" title="Copy link to ${escapeHtml(element.key)}" onclick="event.stopPropagation(); copyPersonLink('${escapeHtml(element.key)}', this)">🔗</span>`; |
| 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> |
| ${copyLinkHtml} |
| ${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 getSelectedTypes() { |
| const selectedTypes = []; |
| if (document.getElementById('selectedLocon').checked) selectedTypes.push('locon'); |
| if (document.getElementById('selectedLora').checked) selectedTypes.push('lora'); |
| if (document.getElementById('selectedEmbedding').checked) selectedTypes.push('embedding'); |
| if (document.getElementById('selectedFlux').checked) selectedTypes.push('flux'); |
| if (document.getElementById('selectedWan').checked) selectedTypes.push('wan'); |
| if (document.getElementById('selectedSdxl').checked) selectedTypes.push('sdxl'); |
| if (document.getElementById('selectedZimage').checked) selectedTypes.push('zimage'); |
| if (document.getElementById('selectedZbase').checked) selectedTypes.push('zbase'); |
| if (document.getElementById('selectedQwen').checked) selectedTypes.push('qwen'); |
| if (document.getElementById('selectedLtx').checked) selectedTypes.push('ltx'); |
| if (document.getElementById('selectedKlein9').checked) selectedTypes.push('klein9'); |
| return selectedTypes; |
| } |
| |
| |
| function formatDateForDisplay(dateStr) { |
| if (!dateStr) return 'Unknown Date'; |
| const date = new Date(dateStr); |
| const today = new Date(); |
| const yesterday = new Date(today); |
| yesterday.setDate(yesterday.getDate() - 1); |
| |
| |
| if (date.toDateString() === today.toDateString()) { |
| return 'Today'; |
| } |
| |
| |
| if (date.toDateString() === yesterday.toDateString()) { |
| return 'Yesterday'; |
| } |
| |
| |
| return date.toLocaleDateString('en-US', { |
| weekday: 'long', |
| year: 'numeric', |
| month: 'long', |
| day: 'numeric' |
| }); |
| } |
| |
| 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', |
| klein9: 'Klein9', |
| wan: 'WAN', |
| sdxl: 'SDXL', |
| ltx: 'LTX', |
| qwen: 'Qwen', |
| zimage: 'Z Image Turbo', |
| zbase: 'Z Image Base' |
| }; |
| |
| |
| 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'; |
| } |
| |
| |
| dateEntries.sort((a, b) => b.sortDate.localeCompare(a.sortDate)); |
| const newestDate = dateEntries[0].sortDate; |
| |
| const lines = ['Update History:']; |
| |
| dateEntries.sort((a, b) => a.name.localeCompare(b.name)); |
| |
| for (const entry of dateEntries) { |
| |
| const isNewest = entry.sortDate === newestDate && dateEntries.length > 1; |
| const prefix = isNewest ? '⭐ ' : ''; |
| lines.push(`${prefix}${entry.name}: ${entry.date}`); |
| } |
| |
| return lines.join('\n'); |
| } |
| |
| |
| function openDailyUploadsModal() { |
| const modal = document.getElementById('dailyUploadsModal'); |
| const dateSelect = document.getElementById('dailyDateSelect'); |
| |
| |
| 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; |
| } |
| |
| |
| const modelTypeOrder = [ |
| 'locon', |
| 'lora', |
| 'embedding', |
| 'flux', |
| 'wan', |
| 'sdxl', |
| 'ltx', |
| 'zimage', |
| 'zbase', |
| 'klein9', |
| 'qwen' |
| ]; |
| const orderIndex = new Map(modelTypeOrder.map((t, i) => [t, i])); |
| |
| 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; |
| } |
| } |
| } |
| } |
| if (modelsOnDate.length > 0) { |
| modelsOnDate.sort((a, b) => { |
| const ai = orderIndex.get(String(a).toLowerCase()) ?? Number.POSITIVE_INFINITY; |
| const bi = orderIndex.get(String(b).toLowerCase()) ?? Number.POSITIVE_INFINITY; |
| if (ai !== bi) return ai - bi; |
| return String(a).localeCompare(String(b)); |
| }); |
| uploadsOnDate.push({ person: personKey, models: modelsOnDate }); |
| } |
| } |
| |
| |
| uploadsOnDate.sort((a, b) => a.person.localeCompare(b.person)); |
| |
| |
| 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"> |
| <div class="daily-upload-person-row"> |
| <span class="daily-upload-person">${escapeHtml(item.person)}</span> |
| <a href="?personcode=${encodeURIComponent(item.person)}" |
| target="_blank" |
| class="person-modal-link" |
| title="Open model in new tab" |
| onclick="event.stopPropagation();"> |
| 🔗 |
| </a> |
| </div> |
| <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', |
| klein9: 'klein9', |
| wan: 'wan', |
| sdxl: 'sdxl', |
| ltx: 'ltx', |
| qwen: 'qwen', |
| zimage: 'zimage', |
| zbase: 'zbase' |
| }; |
| |
| const folder = folderMap[modelType.toLowerCase()]; |
| if (!folder) { |
| return '#'; |
| } |
| |
| |
| const filenamesData = window.filenames || {}; |
| const personFilenames = filenamesData[personKey]; |
| const filenames = personFilenames?.[modelType.toLowerCase()]; |
| |
| if (!filenames || filenames.length === 0) { |
| |
| return `https://huggingface.co/malcolmrey/${folder}`; |
| } |
| |
| const filename = filenames[0]; |
| |
| 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>` : ''; |
| } |
| |
| |
| const links = filenames.map((filename, index) => { |
| |
| 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'); |
| const modalVideo = document.getElementById('modalVideo'); |
| |
| |
| 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; |
| if (modalVideo) { |
| modalVideo.pause?.(); |
| modalVideo.removeAttribute('src'); |
| modalVideo.load?.(); |
| modalVideo.style.display = 'none'; |
| } |
| |
| 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.ltx)} LTX</span> |
| <span class="modal-stat-value"> |
| ${formatModalHFLinks(element.ltx, 'ltx', personFilenames.ltx)} |
| </span> |
| </div> |
| <div class="modal-stat-row"> |
| <span class="modal-stat-label">${yesNo(element.zimage)} Z Image Turbo</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.zbase)} Z Image Base</span> |
| <span class="modal-stat-value"> |
| ${formatModalHFLinks(element.zbase, 'zbase', personFilenames.zbase)} |
| </span> |
| </div> |
| <div class="modal-stat-row"> |
| <span class="modal-stat-label">${yesNo(element.klein9)} Klein9</span> |
| <span class="modal-stat-value"> |
| ${formatModalHFLinks(element.klein9, 'klein9', personFilenames.klein9)} |
| </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(); |
| } |
| } |
| |
| |
| document.addEventListener('keydown', function(event) { |
| if (event.key === 'Escape') { |
| closeModal(); |
| } |
| }); |
| |
| const notMatched = { |
| lycoris: [], |
| lora: [], |
| embedding: [], |
| flux: [], |
| wan: [], |
| sdxl: [], |
| ltx: [], |
| qwen: [], |
| zbase: [], |
| }; |
| |
| 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); |
| } |
| }); |
| |
| if (models.ltxes) { |
| models.ltxes.forEach((ltx) => { |
| const key = prepareKey(ltx.name); |
| if (presence[key] !== undefined) { |
| presence[key].ltxCivitai = true; |
| setImageUrl(key, ltx.imageUrl); |
| presence[key].ltxCivitaiLink = ltx.url; |
| } else if (!isKnownSkippableKey(key)) { |
| if (!notMatched.ltx) { |
| notMatched.ltx = []; |
| } |
| notMatched.ltx.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); |
| |
| |
| for (const key in presence) { |
| const personImages = buildImagesList(key, presence[key]); |
| if (personImages.length > 0) { |
| |
| 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, |
| klein9: presence[property].klein9, |
| wan: presence[property].wan, |
| sdxl: presence[property].sdxl, |
| ltx: presence[property].ltx, |
| zimage: presence[property].zimage, |
| zbase: presence[property].zbase, |
| 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, |
| klein9HFLink: presence[property]?.klein9HFLink, |
| wanHFLink: presence[property]?.wanHFLink, |
| sdxlHFLink: presence[property]?.sdxlHFLink, |
| ltxHFLink: presence[property]?.ltxHFLink, |
| zimageHFLink: presence[property]?.zimageHFLink, |
| zbaseHFLink: presence[property]?.zbaseHFLink, |
| qwenHFLink: presence[property]?.qwenHFLink, |
| }; |
| presenceModels.push(element); |
| } |
| |
| function filterByType(element) { |
| const mode = document.getElementById('searchMode').value; |
| const filters = [ |
| { selected: document.getElementById('selectedLocon').checked, property: element.locon }, |
| { selected: document.getElementById('selectedLora').checked, property: element.lora }, |
| { selected: document.getElementById('selectedEmbedding').checked, property: element.embedding }, |
| { selected: document.getElementById('selectedFlux').checked, property: element.flux }, |
| { selected: document.getElementById('selectedWan').checked, property: element.wan }, |
| { selected: document.getElementById('selectedSdxl').checked, property: element.sdxl }, |
| { selected: document.getElementById('selectedLtx').checked, property: element.ltx }, |
| { selected: document.getElementById('selectedZimage').checked, property: element.zimage }, |
| { selected: document.getElementById('selectedZbase').checked, property: element.zbase }, |
| { selected: document.getElementById('selectedKlein9').checked, property: element.klein9 }, |
| { selected: document.getElementById('selectedQwen').checked, property: element.qwen }, |
| ]; |
| |
| |
| const anySelected = filters.some((f) => f.selected); |
| if (!anySelected) { |
| return true; |
| } |
| |
| |
| for (const filter of filters) { |
| if (filter.selected) { |
| if (mode === 'available' && !filter.property) { |
| return false; |
| } |
| if (mode === 'missing' && filter.property) { |
| return false; |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| function searchModelsModern(value) { |
| const lowerCaseValue = value.toLowerCase(); |
| const sortBy = getSortBy(); |
| const dateRange = getDateRange(); |
| const pageSize = getPageSize(); |
| |
| |
| const selectedTypes = getSelectedTypes(); |
| |
| |
| const showAllTypes = selectedTypes.length === 0; |
| |
| |
| let filtered = presenceModels.filter((element) => { |
| return ( |
| (element.key.toLowerCase().includes(lowerCaseValue) || value === '*') && |
| filterByType(element) |
| ); |
| }); |
| |
| |
| if (dateRange) { |
| filtered = filtered.filter((element) => { |
| |
| const relevantDate = showAllTypes |
| ? getPersonUploadDate(element.key) |
| : getPersonUploadDate(element.key, selectedTypes); |
| |
| |
| |
| if (!showAllTypes && !relevantDate) { |
| return false; |
| } |
| |
| return isWithinDateRange(relevantDate, dateRange); |
| }); |
| } |
| |
| |
| const isSortingByDate = sortBy.includes('date') || sortBy.includes('daily'); |
| filtered = sortModels( |
| filtered, |
| sortBy, |
| (isSortingByDate && !showAllTypes) ? selectedTypes : null |
| ); |
| |
| |
| filteredResults = filtered; |
| |
| |
| if (pageSize === Infinity) { |
| totalPages = 1; |
| } else { |
| totalPages = Math.ceil(filtered.length / pageSize); |
| } |
| |
| |
| if (currentPage > totalPages) { |
| currentPage = Math.max(1, totalPages); |
| } |
| |
| document.getElementById('found').value = filtered.length; |
| |
| renderCurrentPage(); |
| } |
| |
| |
| window.searchModels = searchModelsModern; |
| |
| |
| function copyPersonLink(personKey, iconElement) { |
| const url = new URL(window.location.href); |
| url.searchParams.set('personcode', personKey); |
| |
| navigator.clipboard.writeText(url.toString()).then(() => { |
| |
| iconElement.classList.add('copied'); |
| iconElement.title = 'Link copied!'; |
| setTimeout(() => { |
| iconElement.classList.remove('copied'); |
| iconElement.title = `Copy link to ${personKey}`; |
| }, 2000); |
| }).catch(err => { |
| console.error('Failed to copy link:', err); |
| alert('Failed to copy link to clipboard'); |
| }); |
| } |
| |
| |
| function checkUrlParams() { |
| const urlParams = new URLSearchParams(window.location.search); |
| const personcode = urlParams.get('personcode'); |
| const dateFrom = urlParams.get('dateFrom'); |
| const dateTo = urlParams.get('dateTo'); |
| |
| |
| if (dateFrom) { |
| document.getElementById('dateFrom').value = dateFrom; |
| } |
| if (dateTo) { |
| document.getElementById('dateTo').value = dateTo; |
| } |
| |
| if (personcode) { |
| |
| document.getElementById('search').value = personcode; |
| |
| |
| searchModels(personcode); |
| |
| |
| setTimeout(() => { |
| const lowerPersoncode = personcode.toLowerCase(); |
| const matchingPerson = presenceModels.find(element => |
| element.key.toLowerCase() === lowerPersoncode |
| ); |
| |
| if (matchingPerson) { |
| openModal(matchingPerson); |
| } |
| }, 100); |
| } else { |
| searchModels(getCurrentSearchValue()); |
| } |
| } |
| |
| checkUrlParams(); |
| |
| |
| 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' }; |
| |
| |
| if (personHFImages.ZBase) { |
| images.push(...personHFImages.ZBase); |
| } |
| |
| |
| if (personHFImages.ZImage) { |
| images.push(...personHFImages.ZImage); |
| } |
| |
| |
| if (personHFImages.Klein9) { |
| images.push(...personHFImages.Klein9); |
| } |
| |
| |
| if (personHFImages.WAN) { |
| images.push(...personHFImages.WAN); |
| } |
| |
| |
| if (personHFImages.LTX) { |
| images.push(...personHFImages.LTX); |
| } |
| |
| |
| |
| 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); |
| if (models.ltxes) { |
| checkCivitaiImage('LTX', models.ltxes); |
| } |
| checkCivitaiImage('Qwen', models.qwens); |
| |
| |
| 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 modalVideo = document.getElementById('modalVideo'); |
| 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]; |
| const isVideo = currentImage.url.toLowerCase().endsWith('.mp4'); |
| if (isVideo) { |
| if (modalVideo) { |
| modalVideo.src = currentImage.url; |
| modalVideo.style.display = 'block'; |
| } |
| modalImage.style.display = 'none'; |
| } else { |
| modalImage.src = currentImage.url; |
| modalImage.style.display = 'block'; |
| if (modalVideo) { |
| modalVideo.pause?.(); |
| modalVideo.removeAttribute('src'); |
| modalVideo.load?.(); |
| modalVideo.style.display = 'none'; |
| } |
| } |
| |
| 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'; |
| } |
| |
| const frameworkDisplayName = |
| currentImage.framework === 'ZImage' ? 'ZTURBO' : currentImage.framework; |
| frameworkLabel.textContent = frameworkDisplayName; |
| 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(); |
| } |
| } |
| |
| |
| const originalOpenModal = openModal; |
| openModal = function(element) { |
| currentPersonKey = element.key; |
| currentImages = buildImagesList(element.key, element); |
| currentImageIndex = 0; |
| |
| originalOpenModal(element); |
| |
| if (currentImages.length > 0) { |
| updateImageDisplay(); |
| } |
| }; |
| |
| |
| 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> |
|
|