| <!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="SDXL"] {
|
| background: linear-gradient(135deg, #84cc16 0%, #22c55e 100%);
|
| }
|
|
|
| .framework-label[data-framework="ZImage"] {
|
| background: linear-gradient(135deg, #ec4899 0%, #f97316 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-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>
|
|
|
| <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>
|
|
|
|
|
| <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" />
|
| <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 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();
|
|
|
| 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 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 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'
|
| };
|
|
|
|
|
| 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 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) {
|
| 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">
|
| <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 '#';
|
| }
|
|
|
|
|
| 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 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();
|
| }
|
| }
|
|
|
|
|
| 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);
|
|
|
|
|
| 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,
|
| 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();
|
|
|
|
|
| let filtered = presenceModels.filter((element) => {
|
| return (
|
| (element.key.toLowerCase().includes(lowerCaseValue) || value === '*') &&
|
| filterByType(element)
|
| );
|
| });
|
|
|
|
|
| if (periodFilter !== 'all') {
|
| filtered = filtered.filter((element) => {
|
| const uploadDate = getPersonUploadDate(element.key);
|
| return isWithinPeriod(uploadDate, periodFilter);
|
| });
|
| }
|
|
|
|
|
| filtered = sortModels(filtered, sortBy);
|
|
|
|
|
| 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');
|
|
|
| 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.ZImage) {
|
| images.push(...personHFImages.ZImage);
|
| }
|
|
|
|
|
| if (personHFImages.WAN) {
|
| images.push(...personHFImages.WAN);
|
| }
|
|
|
|
|
|
|
| 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);
|
|
|
|
|
| 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();
|
| }
|
| }
|
|
|
|
|
| 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>
|
|
|