Compare commits

..

6 Commits

Author SHA1 Message Date
Will Miao
faf64f8986 fix(css): migrate duplicates component to canonical color tokens
Replace undefined --lora-accent-l/c/h and --lora-warning-l/c/h with
canonical --color-accent-l/c/h and --color-warning-l/c/h from the
design token system. Fix 5 border-color declarations missing oklch()
wrapper, fix var() space syntax error in .group-toggle-btn:hover,
and replace hardcoded green with --color-success token.
2026-06-18 22:41:46 +08:00
Will Miao
a617487a43 fix(ui): lift theme popover out of header stacking context to appear above modals 2026-06-18 22:19:36 +08:00
Will Miao
3012a7aef3 fix(settings): prevent Firefox save-password prompt from API key input
- Remove server-side value='...' from password field in settings modal template
  so the API key is never baked into the DOM at page load time
- Populate the input dynamically via loadSettingsToUI() when modal opens
- Clear both API key and proxy password fields on modal close to prevent
  Firefox from detecting pre-filled password fields on page navigation
2026-06-18 21:57:03 +08:00
Will Miao
499e19de34 fix(modals): tone down batch summary modal styling - remove icons, flatten gradients, lock to design tokens
- Metadata Fetch Summary: remove per-card icons, demote total/duration cards
  to neutral border, drop title icon, fix table header border width
- Batch Import Summary: replace 3em centered hero with inline left-aligned
  layout, flatten progress bar gradient, simplify circular badges to plain
  colored icons, unify border widths to 4px and token namespace to --color-
- Lock all off-scale em typography to --text-{xs,lg} design tokens
2026-06-18 21:56:58 +08:00
Will Miao
9161762ca9 fix(sidebar): align hidden indicator height (48px) and icon size with sidebar header 2026-06-18 21:14:35 +08:00
Will Miao
9bbd26efe6 feat(license-icons): add second set of license icons matching current CivitAI design
- Add 5 new Tabler SVG icons (currency-dollar, brush, user, git-merge, license)
- Implement Set 2 rendering in ModelModal.js (standalone UI) with green/red
  permission indicators and preview_tooltip.js (ComfyUI widget)
- Add use_new_license_icons setting (default: true) with toggle in settings UI
- ComfyUI tooltip reads setting directly from preview-url API response to
  eliminate race conditions and respect standalone settings changes
- Remove the now-unused separate ComfyUI setting loramanager.license_icon_style
- Add CSS for both standalone (lora-modal.css) and widget (lm_styles.css)
- i18n: translate licenseIcons keys into all 10 supported languages
- Fix test to use classic style explicitly for continued coverage
2026-06-18 21:07:44 +08:00
33 changed files with 573 additions and 177 deletions

View File

@@ -314,6 +314,7 @@
"downloads": "Downloads",
"videoSettings": "Video-Einstellungen",
"layoutSettings": "Layout-Einstellungen",
"licenseIcons": "Lizenzsymbole",
"misc": "Verschiedenes",
"backup": "Backups",
"folderSettings": "Standard-Roots",
@@ -594,6 +595,10 @@
"label": "Früher Zugriff Updates ausblenden",
"help": "Nur Early-Access-Updates"
},
"licenseIcons": {
"useNewStyle": "Aktualisierte Lizenzsymbole verwenden",
"useNewStyleHelp": "Lizenzberechtigungen mit farbigen Indikatoren (neuer Stil) oder nur Einschränkungssymbolen (klassischer Stil) anzeigen. Orientiert sich am aktuellen CivitAI-Design."
},
"misc": {
"includeTriggerWords": "Trigger Words in LoRA-Syntax einschließen",
"includeTriggerWordsHelp": "Trainierte Trigger Words beim Kopieren der LoRA-Syntax in die Zwischenablage einschließen",

View File

@@ -314,6 +314,7 @@
"downloads": "Downloads",
"videoSettings": "Video Settings",
"layoutSettings": "Layout Settings",
"licenseIcons": "License Icons",
"misc": "Miscellaneous",
"backup": "Backups",
"folderSettings": "Default Roots",
@@ -594,6 +595,10 @@
"label": "Hide Early Access Updates",
"help": "When enabled, models with only early access updates will not show 'Update available' badge"
},
"licenseIcons": {
"useNewStyle": "Use updated license icons",
"useNewStyleHelp": "Display license permissions with colored indicators (new style) or restriction-only icons (classic style). Mirroring the current CivitAI design."
},
"misc": {
"includeTriggerWords": "Include Trigger Words in LoRA Syntax",
"includeTriggerWordsHelp": "Include trained trigger words when copying LoRA syntax to clipboard",

View File

@@ -314,6 +314,7 @@
"downloads": "Descargas",
"videoSettings": "Configuración de video",
"layoutSettings": "Configuración de diseño",
"licenseIcons": "Iconos de licencia",
"misc": "Varios",
"backup": "Copias de seguridad",
"folderSettings": "Raíces predeterminadas",
@@ -594,6 +595,10 @@
"label": "Ocultar actualizaciones de acceso temprano",
"help": "Solo actualizaciones de acceso temprano"
},
"licenseIcons": {
"useNewStyle": "Usar iconos de licencia actualizados",
"useNewStyleHelp": "Mostrar permisos de licencia con indicadores de color (nuevo estilo) o solo iconos de restricción (estilo clásico). Refleja el diseño actual de CivitAI."
},
"misc": {
"includeTriggerWords": "Incluir palabras clave en la sintaxis de LoRA",
"includeTriggerWordsHelp": "Incluir palabras clave entrenadas al copiar la sintaxis de LoRA al portapapeles",

View File

@@ -314,6 +314,7 @@
"downloads": "Téléchargements",
"videoSettings": "Paramètres vidéo",
"layoutSettings": "Paramètres d'affichage",
"licenseIcons": "Icônes de licence",
"misc": "Divers",
"backup": "Sauvegardes",
"folderSettings": "Racines par défaut",
@@ -594,6 +595,10 @@
"label": "Masquer les mises à jour en accès anticipé",
"help": "Seulement les mises à jour en accès anticipé"
},
"licenseIcons": {
"useNewStyle": "Utiliser les icônes de licence mises à jour",
"useNewStyleHelp": "Afficher les permissions de licence avec des indicateurs colorés (nouveau style) ou des icônes de restriction uniquement (style classique). Reprend le design actuel de CivitAI."
},
"misc": {
"includeTriggerWords": "Inclure les mots-clés dans la syntaxe LoRA",
"includeTriggerWordsHelp": "Inclure les mots-clés d'entraînement lors de la copie de la syntaxe LoRA dans le presse-papiers",

View File

@@ -314,6 +314,7 @@
"downloads": "הורדות",
"videoSettings": "הגדרות וידאו",
"layoutSettings": "הגדרות פריסה",
"licenseIcons": "סמלי רישיון",
"misc": "שונות",
"backup": "גיבויים",
"folderSettings": "תיקיות ברירת מחדל",
@@ -594,6 +595,10 @@
"label": "הסתר עדכוני גישה מוקדמת",
"help": "רק עדכוני גישה מוקדמת"
},
"licenseIcons": {
"useNewStyle": "השתמש בסמלי רישיון מעודכנים",
"useNewStyleHelp": "הצג הרשאות רישיון עם מחוונים צבעוניים (סגנון חדש) או סמלי הגבלה בלבד (סגנון קלאסי). משקף את העיצוב העדכני של CivitAI."
},
"misc": {
"includeTriggerWords": "כלול מילות טריגר בתחביר LoRA",
"includeTriggerWordsHelp": "כלול מילות טריגר מאומנות בעת העתקת תחביר LoRA ללוח",

View File

@@ -314,6 +314,7 @@
"downloads": "ダウンロード",
"videoSettings": "動画設定",
"layoutSettings": "レイアウト設定",
"licenseIcons": "ライセンスアイコン",
"misc": "その他",
"backup": "バックアップ",
"folderSettings": "デフォルトルート",
@@ -594,6 +595,10 @@
"label": "早期アクセス更新を非表示",
"help": "早期アクセスのみの更新"
},
"licenseIcons": {
"useNewStyle": "更新されたライセンスアイコンを使用",
"useNewStyleHelp": "カラーインジケーター付きでライセンス許可を表示新スタイルするか、制限のみのアイコンを表示クラシックスタイルします。現在のCivitAIデザインを反映しています。"
},
"misc": {
"includeTriggerWords": "LoRA構文にトリガーワードを含める",
"includeTriggerWordsHelp": "LoRA構文をクリップボードにコピーする際、学習済みトリガーワードを含めます",

View File

@@ -314,6 +314,7 @@
"downloads": "다운로드",
"videoSettings": "비디오 설정",
"layoutSettings": "레이아웃 설정",
"licenseIcons": "라이선스 아이콘",
"misc": "기타",
"backup": "백업",
"folderSettings": "기본 루트",
@@ -594,6 +595,10 @@
"label": "얼리 액세스 업데이트 숨기기",
"help": "얼리 액세스 업데이트만"
},
"licenseIcons": {
"useNewStyle": "업데이트된 라이선스 아이콘 사용",
"useNewStyleHelp": "색상 표시기가 있는 라이선스 권한(새 스타일) 또는 제한 전용 아이콘(클래식 스타일)을 표시합니다. 현재 CivitAI 디자인을 반영합니다."
},
"misc": {
"includeTriggerWords": "LoRA 문법에 트리거 단어 포함",
"includeTriggerWordsHelp": "LoRA 문법을 클립보드에 복사할 때 학습된 트리거 단어를 포함합니다",

View File

@@ -314,6 +314,7 @@
"downloads": "Загрузки",
"videoSettings": "Настройки видео",
"layoutSettings": "Настройки макета",
"licenseIcons": "Значки лицензии",
"misc": "Разное",
"backup": "Резервные копии",
"folderSettings": "Корневые папки",
@@ -594,6 +595,10 @@
"label": "Скрыть обновления раннего доступа",
"help": "Только обновления раннего доступа"
},
"licenseIcons": {
"useNewStyle": "Использовать обновлённые значки лицензии",
"useNewStyleHelp": "Отображать разрешения лицензии с цветными индикаторами (новый стиль) или только значки ограничений (классический стиль). Соответствует текущему дизайну CivitAI."
},
"misc": {
"includeTriggerWords": "Включать триггерные слова в синтаксис LoRA",
"includeTriggerWordsHelp": "Включать обученные триггерные слова при копировании синтаксиса LoRA в буфер обмена",

View File

@@ -314,6 +314,7 @@
"downloads": "下载",
"videoSettings": "视频设置",
"layoutSettings": "布局设置",
"licenseIcons": "许可协议图标",
"misc": "其他",
"backup": "备份",
"folderSettings": "默认根目录",
@@ -594,6 +595,10 @@
"label": "隐藏抢先体验更新",
"help": "抢先体验更新"
},
"licenseIcons": {
"useNewStyle": "使用新版许可协议图标",
"useNewStyleHelp": "以彩色指示器显示许可权限(新样式),或仅显示限制图标(经典样式)。与当前 CivitAI 设计保持一致。"
},
"misc": {
"includeTriggerWords": "复制 LoRA 语法时包含触发词",
"includeTriggerWordsHelp": "复制 LoRA 语法到剪贴板时包含训练触发词",

View File

@@ -314,6 +314,7 @@
"downloads": "下載",
"videoSettings": "影片設定",
"layoutSettings": "版面設定",
"licenseIcons": "許可協議圖標",
"misc": "其他",
"backup": "備份",
"folderSettings": "預設根目錄",
@@ -594,6 +595,10 @@
"label": "隱藏搶先體驗更新",
"help": "搶先體驗更新"
},
"licenseIcons": {
"useNewStyle": "使用新版許可協議圖標",
"useNewStyleHelp": "以彩色指示器顯示許可權限(新樣式),或僅顯示限制圖標(經典樣式)。與當前 CivitAI 設計保持一致。"
},
"misc": {
"includeTriggerWords": "在 LoRA 語法中包含觸發詞",
"includeTriggerWordsHelp": "複製 LoRA 語法到剪貼簿時包含訓練觸發詞",

View File

@@ -1272,6 +1272,14 @@ class ModelQueryHandler:
license_flags = (model_data or {}).get("license_flags")
if license_flags is not None:
response_payload["license_flags"] = int(license_flags)
# Include the user's license icon style preference so the
# ComfyUI tooltip can pick the right set without a separate
# API call.
try:
settings = get_settings_manager()
response_payload["use_new_license_icons"] = settings.get("use_new_license_icons", True)
except Exception:
pass
return web.json_response(response_payload)
return web.json_response(
{

View File

@@ -105,6 +105,7 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
"download_skip_base_models": [],
"backup_auto_enabled": True,
"backup_retention_count": 5,
"use_new_license_icons": True,
}

View File

@@ -349,8 +349,8 @@
}
.progress-percentage {
font-size: 1.2em;
font-weight: 600;
font-size: var(--text-lg);
font-weight: var(--weight-semibold);
color: var(--lora-accent);
}
@@ -365,9 +365,9 @@
.progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--lora-accent), oklch(from var(--lora-accent) calc(l + 0.1) c h));
border-radius: 4px;
transition: width 0.3s ease;
background: var(--lora-accent);
border-radius: var(--border-radius-xs);
transition: width var(--transition-base);
}
/* Progress Stats */
@@ -389,27 +389,26 @@
}
.stat-item.success {
border-left: 3px solid var(--color-success);
border-left: 4px solid var(--color-success);
}
.stat-item.failed {
border-left: 3px solid var(--lora-error);
border-left: 4px solid var(--color-error);
}
.stat-item.skipped {
border-left: 3px solid var(--lora-warning);
border-left: 4px solid var(--color-warning);
}
.stat-label {
font-size: 0.8em;
color: var(--text-color);
opacity: 0.7;
font-size: var(--text-xs);
color: var(--text-secondary);
margin-bottom: 4px;
}
.stat-value {
font-size: 1.4em;
font-weight: 600;
font-size: var(--text-lg);
font-weight: var(--weight-semibold);
color: var(--text-color);
}
@@ -425,8 +424,7 @@
}
.current-item-label {
color: var(--text-color);
opacity: 0.7;
color: var(--text-secondary);
flex-shrink: 0;
}
@@ -449,27 +447,29 @@
}
.results-header {
text-align: center;
display: flex;
align-items: center;
gap: var(--space-2);
margin-bottom: var(--space-3);
}
.results-icon {
font-size: 3em;
font-size: var(--text-xl);
color: var(--color-success);
margin-bottom: var(--space-1);
flex-shrink: 0;
}
.results-icon.warning {
color: var(--lora-warning);
color: var(--color-warning);
}
.results-icon.error {
color: var(--lora-error);
color: var(--color-error);
}
.results-title {
font-size: 1.3em;
font-weight: 600;
font-size: var(--text-lg);
font-weight: var(--weight-semibold);
color: var(--text-color);
}
@@ -493,27 +493,26 @@
}
.result-card.success {
border-left: 3px solid var(--color-success);
border-left: 4px solid var(--color-success);
}
.result-card.failed {
border-left: 3px solid var(--lora-error);
border-left: 4px solid var(--color-error);
}
.result-card.skipped {
border-left: 3px solid var(--lora-warning);
border-left: 4px solid var(--color-warning);
}
.result-label {
font-size: 0.8em;
color: var(--text-color);
opacity: 0.7;
font-size: var(--text-xs);
color: var(--text-secondary);
margin-bottom: 4px;
}
.result-value {
font-size: 1.4em;
font-weight: 600;
font-size: var(--text-lg);
font-weight: var(--weight-semibold);
color: var(--text-color);
}
@@ -527,13 +526,13 @@
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 10px;
gap: var(--space-2);
padding: var(--space-2);
cursor: pointer;
color: var(--lora-accent);
font-weight: 500;
font-weight: var(--weight-medium);
border-radius: var(--border-radius-xs);
transition: background 0.2s;
transition: background var(--transition-base);
}
.details-toggle:hover {
@@ -541,7 +540,7 @@
}
.details-toggle i {
transition: transform 0.2s;
transition: transform var(--transition-base);
}
.details-toggle.expanded i {
@@ -561,10 +560,10 @@
.result-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
gap: var(--space-2);
padding: var(--space-2) var(--space-3);
border-bottom: 1px solid var(--border-color);
font-size: 0.9em;
font-size: var(--text-sm);
}
.result-item:last-child {
@@ -572,28 +571,23 @@
}
.result-item-status {
width: 24px;
height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.8em;
font-size: var(--text-sm);
flex-shrink: 0;
}
.result-item-status.success {
background: color-mix(in oklch, var(--color-success) 20%, transparent);
color: var(--color-success);
}
.result-item-status.failed {
background: oklch(from var(--lora-error) l c h / 0.2);
color: var(--lora-error);
color: var(--color-error);
}
.result-item-status.skipped {
background: oklch(from var(--lora-warning) l c h / 0.2);
color: var(--lora-warning);
color: var(--color-warning);
}
.result-item-info {
@@ -610,8 +604,8 @@
}
.result-item-error {
font-size: 0.8em;
color: var(--lora-error);
font-size: var(--text-xs);
color: var(--color-error);
margin-top: 2px;
}

View File

@@ -5,10 +5,10 @@
position: sticky; /* Keep the sticky position */
top: var(--space-1);
width: 100%;
background-color: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.1); /* Use accent color with low opacity */
background-color: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h) / 0.1); /* Use accent color with low opacity */
color: var(--text-color);
border-top: 1px solid oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.3); /* Add top border with accent color */
border-bottom: 1px solid oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.4); /* Make bottom border stronger */
border-top: 1px solid oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h) / 0.3); /* Add top border with accent color */
border-bottom: 1px solid oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h) / 0.4); /* Make bottom border stronger */
z-index: var(--z-overlay);
padding: 12px 0;
box-shadow: var(--shadow-lg); /* Stronger shadow */
@@ -41,7 +41,7 @@
.duplicates-banner i.fa-exclamation-triangle {
font-size: 18px;
color: oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h));
color: oklch(var(--color-warning-l) var(--color-warning-c) var(--color-warning-h));
}
.duplicates-banner .banner-actions {
@@ -70,7 +70,7 @@
.duplicates-banner button.btn-exit-mode:hover {
background-color: var(--bg-color);
border-color: var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h);
border-color: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h));
transform: translateY(-1px);
}
@@ -92,7 +92,7 @@
}
.duplicates-banner button:hover {
border-color: var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h);
border-color: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h));
background: var(--bg-color);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
@@ -117,7 +117,7 @@
/* Duplicate groups */
.duplicate-group {
position: relative;
border: 2px solid oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h));
border: 2px solid oklch(var(--color-warning-l) var(--color-warning-c) var(--color-warning-h));
border-radius: var(--border-radius-base);
padding: 16px;
margin-bottom: 24px;
@@ -152,7 +152,7 @@
display: flex;
justify-content: space-between;
align-items: center;
border-left: 4px solid oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h)); /* Add accent border on the left */
border-left: 4px solid oklch(var(--color-warning-l) var(--color-warning-c) var(--color-warning-h)); /* Add accent border on the left */
}
.duplicate-group-header span:last-child {
@@ -180,7 +180,7 @@
}
.duplicate-group-header button:hover {
border-color: var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h);
border-color: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h));
background: var(--bg-color);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
@@ -235,7 +235,7 @@
}
.group-toggle-btn:hover {
border-color: var(--lora-accent-l) var(--lora-accent-c) var (--lora-accent-h);
border-color: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h));
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
@@ -247,16 +247,16 @@
}
.model-card.duplicate:hover {
border-color: var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h);
border-color: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h));
}
.model-card.duplicate.latest {
border-style: solid;
border-color: oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h));
border-color: oklch(var(--color-warning-l) var(--color-warning-c) var(--color-warning-h));
}
.model-card.duplicate-selected {
border: 2px solid oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h));
border: 2px solid oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h));
box-shadow: var(--shadow-md);
}
@@ -276,7 +276,7 @@
position: absolute;
top: 10px;
left: 10px;
background: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h));
background: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h));
color: white;
font-size: 12px;
padding: 2px 6px;
@@ -328,7 +328,7 @@
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed var(--border-color);
color: oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h));
color: oklch(var(--color-warning-l) var(--color-warning-c) var(--color-warning-h));
font-weight: bold;
word-break: break-all; /* Ensure long hashes wrap properly */
}
@@ -351,12 +351,12 @@
}
.verification-badge.verified {
background-color: oklch(70% 0.2 140); /* Green for verified */
background-color: var(--color-success); /* Green for verified */
color: white;
}
.verification-badge.mismatch {
background-color: oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h));
background-color: oklch(var(--color-warning-l) var(--color-warning-c) var(--color-warning-h));
color: white;
}
@@ -366,7 +366,7 @@
/* Hash Mismatch Styling */
.model-card.duplicate.hash-mismatch {
border: 2px dashed oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h));
border: 2px dashed oklch(var(--color-warning-l) var(--color-warning-c) var(--color-warning-h));
opacity: 0.85;
position: relative;
}
@@ -380,8 +380,8 @@
bottom: 0;
background: repeating-linear-gradient(
45deg,
oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h) / 0.05),
oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h) / 0.05) 10px,
oklch(var(--color-warning-l) var(--color-warning-c) var(--color-warning-h) / 0.05),
oklch(var(--color-warning-l) var(--color-warning-c) var(--color-warning-h) / 0.05) 10px,
transparent 10px,
transparent 20px
);
@@ -398,7 +398,7 @@
position: absolute;
top: 10px;
left: 10px; /* Changed from right:10px to left:10px */
background: oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h));
background: oklch(var(--color-warning-l) var(--color-warning-c) var(--color-warning-h));
color: white;
font-size: 12px;
padding: 3px 8px;
@@ -417,7 +417,7 @@
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed var(--border-color);
color: oklch(var(--lora-warning-l) var(--lora-warning-c) var(--lora-warning-h));
color: oklch(var(--color-warning-l) var(--color-warning-c) var(--color-warning-h));
font-weight: bold;
}
@@ -437,7 +437,7 @@
.btn-verify-hashes:hover {
background: var(--bg-color);
border-color: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h));
border-color: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h));
transform: translateY(-1px);
}
@@ -498,7 +498,7 @@
.help-icon:hover {
opacity: 1;
color: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h));
color: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h));
}
/* Help tooltip */
@@ -573,7 +573,7 @@
/* In dark mode, add additional distinction */
html[data-theme="dark"] .duplicates-banner {
box-shadow: var(--shadow-dark-lg); /* Stronger shadow in dark mode */
background-color: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.15); /* Slightly stronger background in dark mode */
background-color: oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h) / 0.15); /* Slightly stronger background in dark mode */
}
html[data-theme="dark"] .duplicate-group {
@@ -598,11 +598,11 @@ html[data-theme="dark"] .help-tooltip {
background: var(--lora-accent);
color: white;
border-color: var(--lora-accent);
box-shadow: 0 0 0 2px oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.25);
box-shadow: 0 0 0 2px oklch(var(--color-accent-l) var(--color-accent-c) var(--color-accent-h) / 0.25);
position: relative;
z-index: 5;
}
#findDuplicatesBtn.active:hover {
background: oklch(calc(var(--lora-accent-l) - 5%) var(--lora-accent-c) var(--lora-accent-h));
background: oklch(calc(var(--color-accent-l) - 5%) var(--color-accent-c) var(--color-accent-h));
}

View File

@@ -329,16 +329,14 @@
.theme-popover {
display: none;
position: absolute;
top: calc(100% + 8px);
right: -8px;
position: fixed;
background: var(--surface-base, #ffffff);
border: 1px solid var(--border-base, #e0e0e0);
border-radius: var(--radius-md, 8px);
box-shadow: var(--shadow-xl, 0 4px 16px rgba(0, 0, 0, 0.15));
padding: 12px;
min-width: 220px;
z-index: var(--z-dropdown, 200);
z-index: calc(var(--z-overlay) + 1);
animation: theme-popover-in 0.15s ease-out;
}

View File

@@ -72,6 +72,10 @@
margin-left: auto;
}
.modal-header-actions .license-permissions {
margin-left: auto;
}
.license-restrictions {
display: flex;
align-items: center;
@@ -95,6 +99,41 @@
transform: translateY(-1px);
}
/* Set 2 — New style permission indicators */
.license-permissions {
display: flex;
gap: 4px;
align-items: center;
}
.license-icon-new {
width: 22px;
height: 22px;
display: inline-block;
border-radius: 4px;
background-color: var(--text-muted);
-webkit-mask: var(--license-icon-image) center/contain no-repeat;
mask: var(--license-icon-image) center/contain no-repeat;
transition: background-color 0.2s ease, transform 0.2s ease;
cursor: default;
outline: 2px solid transparent;
outline-offset: 1px;
}
.license-icon-new.allowed {
background-color: var(--color-success, #40c057);
outline-color: color-mix(in oklch, var(--color-success, #40c057) 30%, transparent);
}
.license-icon-new.denied {
background-color: var(--color-error, #fa5252);
outline-color: color-mix(in oklch, var(--color-error, #fa5252) 30%, transparent);
}
.license-icon-new:hover {
transform: translateY(-1px);
}
/* Info Grid */
.info-grid {
display: grid;

View File

@@ -24,11 +24,6 @@
min-width: 130px;
}
.stat-card > i {
font-size: 1.25em;
flex-shrink: 0;
}
.stat-card-body {
display: flex;
flex-direction: column;
@@ -52,40 +47,20 @@
border-left-color: var(--color-success);
}
.stat-card-success > i {
color: var(--color-success);
}
.stat-card-failure {
border-left-color: var(--color-error);
}
.stat-card-failure > i {
color: var(--color-error);
}
.stat-card-skipped {
border-left-color: var(--color-warning);
}
.stat-card-skipped > i {
color: var(--color-warning);
}
.stat-card-total {
border-left-color: var(--color-info);
}
.stat-card-total > i {
color: var(--color-info);
border-left-color: var(--lora-border);
}
.stat-card-time {
border-left-color: var(--color-accent);
}
.stat-card-time > i {
color: var(--color-accent);
border-left-color: var(--lora-border);
}
.refresh-failures-section {
@@ -122,7 +97,7 @@
position: sticky;
top: 0;
background: var(--lora-surface);
border-bottom: 2px solid var(--lora-border);
border-bottom: 1px solid var(--lora-border);
padding: var(--space-1) var(--space-2);
text-align: left;
font-weight: var(--weight-semibold);

View File

@@ -117,7 +117,7 @@
top: 68px; /* Align with sidebar header */
z-index: var(--z-overlay);
width: 14px;
height: 44px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
@@ -134,7 +134,7 @@
}
.sidebar-hidden-indicator i {
font-size: 9px;
font-size: 11px;
color: var(--text-muted);
transition: color 0.15s ease;
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-brush"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 21v-4a4 4 0 1 1 4 4h-4" /><path d="M21 3a16 16 0 0 0 -12.8 10.2" /><path d="M21 3a16 16 0 0 1 -10.2 12.8" /><path d="M10.6 9a9 9 0 0 1 4.4 4.4" /></svg>

After

Width:  |  Height:  |  Size: 460 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-currency-dollar"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M16.7 8a3 3 0 0 0 -2.7 -2h-4a3 3 0 0 0 0 6h4a3 3 0 0 1 0 6h-4a3 3 0 0 1 -2.7 -2" /><path d="M12 3v3m0 12v3" /></svg>

After

Width:  |  Height:  |  Size: 431 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-git-merge"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M5 18a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M5 6a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M15 12a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" /><path d="M7 8l0 8" /><path d="M7 8a4 4 0 0 0 4 4h4" /></svg>

After

Width:  |  Height:  |  Size: 501 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-license"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 21h-9a3 3 0 0 1 -3 -3v-1h10v2a2 2 0 0 0 4 0v-14a2 2 0 1 1 2 2h-2m2 -4h-11a3 3 0 0 0 -3 3v11" /><path d="M9 7l4 0" /><path d="M9 11l4 0" /></svg>

After

Width:  |  Height:  |  Size: 455 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-user"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M8 7a4 4 0 1 0 8 0a4 4 0 0 0 -8 0" /><path d="M6 21v-2a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v2" /></svg>

After

Width:  |  Height:  |  Size: 401 B

View File

@@ -679,39 +679,34 @@ export class BaseModelApiClient {
<div class="modal-content metadata-refresh-result-modal">
<button class="close" data-action="close-modal">&times;</button>
<h2><i class="fas fa-sync-alt"></i> ${translate('modals.metadataFetchSummary.title', {}, 'Metadata Fetch Summary')}</h2>
<h2>${translate('modals.metadataFetchSummary.title', {}, 'Metadata Fetch Summary')}</h2>
<div class="refresh-summary-stats">
<div class="stat-card stat-card-success">
<i class="fas fa-check-circle"></i>
<div class="stat-card-body">
<span class="stat-card-label">${translate('modals.metadataFetchSummary.statSuccess', {}, 'Success')}</span>
<span class="stat-card-value">${success}</span>
</div>
</div>
<div class="stat-card stat-card-failure">
<i class="fas fa-times-circle"></i>
<div class="stat-card-body">
<span class="stat-card-label">${translate('modals.metadataFetchSummary.statFailed', {}, 'Failed')}</span>
<span class="stat-card-value">${failure_count}</span>
</div>
</div>
<div class="stat-card stat-card-skipped">
<i class="fas fa-forward"></i>
<div class="stat-card-body">
<span class="stat-card-label">${translate('modals.metadataFetchSummary.statSkipped', {}, 'Skipped')}</span>
<span class="stat-card-value">${skipped_count}</span>
</div>
</div>
<div class="stat-card stat-card-total">
<i class="fas fa-database"></i>
<div class="stat-card-body">
<span class="stat-card-label">${translate('modals.metadataFetchSummary.statTotal', {}, 'Total Scanned')}</span>
<span class="stat-card-value">${total || processed}</span>
</div>
</div>
<div class="stat-card stat-card-time">
<i class="fas fa-clock"></i>
<div class="stat-card-body">
<span class="stat-card-label">${translate('modals.metadataFetchSummary.statDuration', {}, 'Duration')}</span>
<span class="stat-card-value">${elapsed_seconds}s</span>

View File

@@ -121,11 +121,11 @@ export class HeaderManager {
this.updatePopoverActiveStates(currentTheme, currentPreset);
themeToggle.addEventListener('click', (e) => {
if (e.target.closest('.theme-popover')) return;
e.stopPropagation();
const isOpen = themePopover.classList.contains('active');
this.closeAllPopovers();
if (!isOpen) {
this.positionThemePopover();
themePopover.classList.add('active');
}
});
@@ -149,6 +149,13 @@ export class HeaderManager {
themePopover.classList.remove('active');
}
});
// Reposition on resize while popover is active
window.addEventListener('resize', () => {
if (themePopover.classList.contains('active')) {
this.positionThemePopover();
}
});
}
closeAllPopovers() {
@@ -158,6 +165,17 @@ export class HeaderManager {
}
}
positionThemePopover() {
const themeToggle = document.querySelector('.theme-toggle');
const themePopover = document.getElementById('themePopover');
if (!themeToggle || !themePopover) return;
const rect = themeToggle.getBoundingClientRect();
// Guard: toggle may be hidden on narrow viewports (≤950px CSS hides .header-controls)
if (rect.width === 0 || rect.height === 0) return;
themePopover.style.top = (rect.bottom + 8) + 'px';
themePopover.style.right = (window.innerWidth - rect.right - 8) + 'px';
}
setThemeMode(mode) {
setStorageItem('theme', mode);
const htmlElement = document.documentElement;

View File

@@ -234,6 +234,95 @@ function renderLicenseIcons(modelData) {
</div>`;
}
// ── Set 2 (new CivitAI-style) permission icons ──
const NEW_LICENSE_ICON_CONFIG = [
{
key: 'commercial',
icon: 'currency-dollar.svg',
allowedFn: (license) => {
const uses = license.allowCommercialUse || [];
return uses.includes('Image') || uses.includes('Sell');
},
labelAllowed: 'Commercial use allowed',
labelDenied: 'No commercial use'
},
{
key: 'genServices',
icon: 'brush.svg',
allowedFn: (license) => {
const uses = license.allowCommercialUse || [];
return uses.includes('RentCivit') || uses.includes('Rent');
},
labelAllowed: 'Generation services allowed',
labelDenied: 'No generation services'
},
{
key: 'credit',
icon: 'user.svg',
allowedFn: (license) => !!license.allowNoCredit,
labelAllowed: 'No credit required',
labelDenied: 'Creator credit required'
},
{
key: 'derivatives',
icon: 'git-merge.svg',
allowedFn: (license) => !!license.allowDerivatives,
labelAllowed: 'Merges allowed',
labelDenied: 'No merges allowed'
},
{
key: 'relicense',
icon: 'license.svg',
allowedFn: (license) => !!license.allowDifferentLicense,
labelAllowed: 'Different permissions allowed on merges',
labelDenied: 'Same permissions required on merges'
}
];
function createNewLicenseIconMarkup(icon, allowed, label) {
const safeLabel = escapeAttribute(label);
const iconPath = `/loras_static/images/tabler/${icon}`;
const stateClass = allowed ? 'allowed' : 'denied';
return `<span class="license-icon-new ${stateClass}" role="img" aria-label="${safeLabel}" title="${safeLabel}" style="--license-icon-image: url('${iconPath}')"></span>`;
}
function renderNewLicenseIcons(modelData) {
const license = modelData?.civitai?.model;
if (!license) {
return '';
}
const icons = [];
NEW_LICENSE_ICON_CONFIG.forEach((config) => {
if (config.key === 'credit' && !hasLicenseField(license, 'allowNoCredit')) {
return;
}
if (config.key === 'derivatives' && !hasLicenseField(license, 'allowDerivatives')) {
return;
}
if (config.key === 'relicense' && !hasLicenseField(license, 'allowDifferentLicense')) {
return;
}
if ((config.key === 'commercial' || config.key === 'genServices') && !hasLicenseField(license, 'allowCommercialUse')) {
return;
}
const allowed = config.allowedFn(license);
const label = allowed ? config.labelAllowed : config.labelDenied;
icons.push(createNewLicenseIconMarkup(config.icon, allowed, label));
});
if (!icons.length) {
return '';
}
const containerLabel = translate('modals.model.license.restrictionsLabel', {}, 'License permissions');
const safeContainerLabel = escapeAttribute(containerLabel);
return `<div class="license-permissions" aria-label="${safeContainerLabel}" role="group">
${icons.join('\n ')}
</div>`;
}
/**
* Display the model modal with the given model data
* @param {Object} model - Model data object
@@ -264,7 +353,10 @@ export async function showModelModal(model, modelType) {
};
const escapedFilePathAttr = escapeAttribute(modelWithFullData.file_path || '');
const escapedFolderPath = escapeHtml((modelWithFullData.file_path || '').replace(/[^/]+$/, '') || 'N/A');
const licenseIcons = renderLicenseIcons(modelWithFullData);
const useNewIcons = state.global.settings.use_new_license_icons !== false;
const licenseIcons = useNewIcons
? renderNewLicenseIcons(modelWithFullData)
: renderLicenseIcons(modelWithFullData);
const viewOnCivitaiAction = modelWithFullData.from_civitai ? `
<div class="civitai-view" title="${translate('modals.model.actions.viewOnCivitai', {}, 'View on Civitai')}" data-action="view-civitai" data-filepath="${escapedFilePathAttr}">
<i class="fas fa-globe"></i> ${translate('modals.model.actions.viewOnCivitaiText', {}, 'View on Civitai')}

View File

@@ -344,9 +344,14 @@ export class SettingsManager {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
this.isOpen = settingsModal.style.display === 'block';
// When modal is opened, update checkbox state from current settings
if (this.isOpen) {
this.loadSettingsToUI();
} else {
// Clear sensitive fields on close to prevent browser save-password prompts
const apiKeyInput = document.getElementById('civitaiApiKey');
if (apiKeyInput) apiKeyInput.value = '';
const proxyPasswordInput = document.getElementById('proxyPassword');
if (proxyPasswordInput) proxyPasswordInput.value = '';
}
}
});
@@ -820,6 +825,11 @@ export class SettingsManager {
usePortableCheckbox.checked = !!state.global.settings.use_portable_settings;
}
const civitaiApiKeyInput = document.getElementById('civitaiApiKey');
if (civitaiApiKeyInput) {
civitaiApiKeyInput.value = state.global.settings.civitai_api_key || '';
}
const civitaiHostSelect = document.getElementById('civitaiHost');
if (civitaiHostSelect) {
civitaiHostSelect.value = state.global.settings.civitai_host || 'civitai.com';
@@ -1003,6 +1013,12 @@ export class SettingsManager {
this.loadDownloadBackendSettings();
this.loadProxySettings();
// Set license icon style
const useNewLicenseIconsCheckbox = document.getElementById('useNewLicenseIcons');
if (useNewLicenseIconsCheckbox) {
useNewLicenseIconsCheckbox.checked = state.global.settings.use_new_license_icons !== false;
}
}
loadDownloadBackendSettings() {
@@ -2947,6 +2963,10 @@ export class SettingsManager {
const showVersionOnCard = state.global.settings.show_version_on_card !== false;
document.body.classList.toggle('hide-card-version', !showVersionOnCard);
// Apply license icon style
const useNewLicenseIcons = state.global.settings.use_new_license_icons !== false;
document.body.classList.toggle('use-new-license-icons', useNewLicenseIcons);
}
}

View File

@@ -52,6 +52,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({
backup_auto_enabled: true,
backup_retention_count: 5,
strip_lora_on_copy: false,
use_new_license_icons: true,
});
export function createDefaultSettings() {

View File

@@ -72,55 +72,6 @@
<i class="fas fa-moon dark-icon"></i>
<i class="fas fa-sun light-icon"></i>
<i class="fas fa-adjust auto-icon"></i>
<div class="theme-popover" id="themePopover">
<div class="theme-popover-section">
<div class="theme-popover-label">{{ t('header.theme.mode') }}</div>
<div class="theme-popover-modes">
<button class="theme-mode-btn" data-mode="light" title="{{ t('header.theme.light') }}">
<i class="fas fa-sun"></i>
<span>{{ t('header.theme.light') }}</span>
</button>
<button class="theme-mode-btn" data-mode="dark" title="{{ t('header.theme.dark') }}">
<i class="fas fa-moon"></i>
<span>{{ t('header.theme.dark') }}</span>
</button>
<button class="theme-mode-btn" data-mode="auto" title="{{ t('header.theme.auto') }}">
<i class="fas fa-adjust"></i>
<span>{{ t('header.theme.auto') }}</span>
</button>
</div>
</div>
<div class="theme-popover-divider"></div>
<div class="theme-popover-section">
<div class="theme-popover-label">{{ t('header.theme.presets') }}</div>
<div class="theme-popover-presets">
<button class="theme-preset-btn" data-preset="default" title="{{ t('header.theme.default') }}">
<span class="preset-swatch preset-swatch-default"></span>
<span>{{ t('header.theme.default') }}</span>
</button>
<button class="theme-preset-btn" data-preset="nord" title="{{ t('header.theme.nord') }}">
<span class="preset-swatch preset-swatch-nord"></span>
<span>{{ t('header.theme.nord') }}</span>
</button>
<button class="theme-preset-btn" data-preset="midnight" title="{{ t('header.theme.midnight') }}">
<span class="preset-swatch preset-swatch-midnight"></span>
<span>{{ t('header.theme.midnight') }}</span>
</button>
<button class="theme-preset-btn" data-preset="monokai" title="{{ t('header.theme.monokai') }}">
<span class="preset-swatch preset-swatch-monokai"></span>
<span>{{ t('header.theme.monokai') }}</span>
</button>
<button class="theme-preset-btn" data-preset="dracula" title="{{ t('header.theme.dracula') }}">
<span class="preset-swatch preset-swatch-dracula"></span>
<span>{{ t('header.theme.dracula') }}</span>
</button>
<button class="theme-preset-btn" data-preset="solarized" title="{{ t('header.theme.solarized') }}">
<span class="preset-swatch preset-swatch-solarized"></span>
<span>{{ t('header.theme.solarized') }}</span>
</button>
</div>
</div>
</div>
</div>
<div class="settings-toggle" title="{{ t('common.actions.settings') }}">
<i class="fas fa-cog"></i>
@@ -169,6 +120,56 @@
</div>
</header>
<div class="theme-popover" id="themePopover" role="dialog" aria-label="{{ t('header.theme.toggle') }}">
<div class="theme-popover-section">
<div class="theme-popover-label">{{ t('header.theme.mode') }}</div>
<div class="theme-popover-modes">
<button class="theme-mode-btn" data-mode="light" title="{{ t('header.theme.light') }}">
<i class="fas fa-sun"></i>
<span>{{ t('header.theme.light') }}</span>
</button>
<button class="theme-mode-btn" data-mode="dark" title="{{ t('header.theme.dark') }}">
<i class="fas fa-moon"></i>
<span>{{ t('header.theme.dark') }}</span>
</button>
<button class="theme-mode-btn" data-mode="auto" title="{{ t('header.theme.auto') }}">
<i class="fas fa-adjust"></i>
<span>{{ t('header.theme.auto') }}</span>
</button>
</div>
</div>
<div class="theme-popover-divider"></div>
<div class="theme-popover-section">
<div class="theme-popover-label">{{ t('header.theme.presets') }}</div>
<div class="theme-popover-presets">
<button class="theme-preset-btn" data-preset="default" title="{{ t('header.theme.default') }}">
<span class="preset-swatch preset-swatch-default"></span>
<span>{{ t('header.theme.default') }}</span>
</button>
<button class="theme-preset-btn" data-preset="nord" title="{{ t('header.theme.nord') }}">
<span class="preset-swatch preset-swatch-nord"></span>
<span>{{ t('header.theme.nord') }}</span>
</button>
<button class="theme-preset-btn" data-preset="midnight" title="{{ t('header.theme.midnight') }}">
<span class="preset-swatch preset-swatch-midnight"></span>
<span>{{ t('header.theme.midnight') }}</span>
</button>
<button class="theme-preset-btn" data-preset="monokai" title="{{ t('header.theme.monokai') }}">
<span class="preset-swatch preset-swatch-monokai"></span>
<span>{{ t('header.theme.monokai') }}</span>
</button>
<button class="theme-preset-btn" data-preset="dracula" title="{{ t('header.theme.dracula') }}">
<span class="preset-swatch preset-swatch-dracula"></span>
<span>{{ t('header.theme.dracula') }}</span>
</button>
<button class="theme-preset-btn" data-preset="solarized" title="{{ t('header.theme.solarized') }}">
<span class="preset-swatch preset-swatch-solarized"></span>
<span>{{ t('header.theme.solarized') }}</span>
</button>
</div>
</div>
</div>
<!-- Add search options panel with context-aware options -->
<div id="searchOptionsPanel" class="search-options-panel hidden">
<div class="options-header">

View File

@@ -103,7 +103,6 @@
<input type="password"
id="civitaiApiKey"
placeholder="{{ t('settings.civitaiApiKeyPlaceholder') }}"
value="{{ settings.get('civitai_api_key', '') }}"
autocomplete="new-password"
onblur="settingsManager.saveInputSetting('civitaiApiKey', 'civitai_api_key')"
onkeydown="if(event.key === 'Enter') { this.blur(); }" />
@@ -592,6 +591,30 @@
</div>
</div>
<!-- License Icons -->
<div class="settings-subsection">
<div class="settings-subsection-header">
<h4>{{ t('settings.sections.licenseIcons') }}</h4>
</div>
<div class="setting-item">
<div class="setting-row">
<div class="setting-info">
<label for="useNewLicenseIcons">
{{ t('settings.licenseIcons.useNewStyle') }}
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.licenseIcons.useNewStyleHelp') }}"></i>
</label>
</div>
<div class="setting-control">
<label class="toggle-switch">
<input type="checkbox" id="useNewLicenseIcons"
onchange="settingsManager.saveToggleSetting('useNewLicenseIcons', 'use_new_license_icons')">
<span class="toggle-slider"></span>
</label>
</div>
</div>
</div>
</div>
<!-- Miscellaneous -->
<div class="settings-subsection">
<div class="settings-subsection-header">

View File

@@ -101,14 +101,19 @@ vi.mock(API_FACTORY, () => ({
describe('Model modal license rendering', () => {
let getModelApiClient;
let state;
beforeEach(async () => {
document.body.innerHTML = '';
({ getModelApiClient } = await import(API_FACTORY));
getModelApiClient.mockReset();
// Import state and force classic icons for this test
const stateModule = await import('../../../static/js/state/index.js');
state = stateModule.state;
state.global.settings.use_new_license_icons = false;
});
it('handles aggregated commercial strings without extra restrictions', async () => {
it('handles aggregated commercial strings without extra restrictions (classic style)', async () => {
const fetchModelMetadata = vi.fn().mockResolvedValue(null);
getModelApiClient.mockReturnValue({
fetchModelMetadata,

View File

@@ -73,6 +73,40 @@
mask: var(--license-icon-image) center/contain no-repeat;
}
/* Set 2 — new style license overlay */
.lm-tooltip__license-overlay-new {
position: absolute;
top: 8px;
left: 8px;
display: flex;
flex-wrap: wrap;
gap: 4px;
padding: 5px 8px;
border-radius: 999px;
background: rgba(10, 10, 14, 0.78);
border: 1px solid rgba(255, 255, 255, 0.12);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
max-width: calc(100% - 16px);
}
.lm-tooltip__license-icon-new {
width: 16px;
height: 16px;
display: inline-block;
border-radius: 3px;
-webkit-mask: var(--license-icon-image) center/contain no-repeat;
mask: var(--license-icon-image) center/contain no-repeat;
}
.lm-tooltip__license-icon-new.allowed {
background-color: #40c057;
}
.lm-tooltip__license-icon-new.denied {
background-color: #fa5252;
}
.lm-loras-container {
display: flex;
flex-direction: column;

View File

@@ -12,6 +12,8 @@ const LICENSE_FLAG_BITS = {
allowRelicense: 1 << 6,
};
// ── Set 1 (classic) icon definitions ──
const LICENSE_ICON_COPY = {
credit: "Creator credit required",
image: "No selling generated content",
@@ -29,6 +31,51 @@ const COMMERCIAL_ICON_CONFIG = [
{ bit: LICENSE_FLAG_BITS.allowSellingModels, icon: "shopping-cart-off.svg", label: LICENSE_ICON_COPY.sell },
];
// ── Set 2 (new CivitAI-style) icon definitions ──
const LNI = LICENSE_ICON_PATH; // alias for brevity
const NEW_LICENSE_ICON_COPY = {
commercial: { allowed: "Commercial use allowed", denied: "No commercial use" },
genServices: { allowed: "Generation services allowed", denied: "No generation services" },
credit: { allowed: "No credit required", denied: "Creator credit required" },
derivatives: { allowed: "Merges allowed", denied: "No merges allowed" },
relicense: { allowed: "Different permissions allowed on merges", denied: "Same permissions required on merges" },
};
const NEW_ICON_CONFIG = [
{
bitCombo: [LICENSE_FLAG_BITS.allowOnImages, LICENSE_FLAG_BITS.allowSellingModels],
icon: "currency-dollar.svg",
labelKey: "commercial",
allowedFn: (flags) => (flags & LICENSE_FLAG_BITS.allowOnImages) !== 0 || (flags & LICENSE_FLAG_BITS.allowSellingModels) !== 0,
},
{
bitCombo: [LICENSE_FLAG_BITS.allowOnCivitai, LICENSE_FLAG_BITS.allowRental],
icon: "brush.svg",
labelKey: "genServices",
allowedFn: (flags) => (flags & LICENSE_FLAG_BITS.allowOnCivitai) !== 0 || (flags & LICENSE_FLAG_BITS.allowRental) !== 0,
},
{
bitCombo: [LICENSE_FLAG_BITS.allowNoCredit],
icon: "user.svg",
labelKey: "credit",
allowedFn: (flags) => (flags & LICENSE_FLAG_BITS.allowNoCredit) !== 0,
},
{
bitCombo: [LICENSE_FLAG_BITS.allowDerivatives],
icon: "git-merge.svg",
labelKey: "derivatives",
allowedFn: (flags) => (flags & LICENSE_FLAG_BITS.allowDerivatives) !== 0,
},
{
bitCombo: [LICENSE_FLAG_BITS.allowRelicense],
icon: "license.svg",
labelKey: "relicense",
allowedFn: (flags) => (flags & LICENSE_FLAG_BITS.allowRelicense) !== 0,
},
];
function parseLicenseFlags(value) {
if (typeof value === "number") {
return Number.isFinite(value) ? value : null;
@@ -78,6 +125,81 @@ function createLicenseIconElement({ icon, label }) {
return element;
}
// ── Set 2 (new style) helpers ──
function buildNewLicenseIconData(licenseFlags) {
if (licenseFlags == null) {
return [];
}
return NEW_ICON_CONFIG.map((config) => {
const allowed = config.allowedFn(licenseFlags);
const label = allowed
? NEW_LICENSE_ICON_COPY[config.labelKey].allowed
: NEW_LICENSE_ICON_COPY[config.labelKey].denied;
return {
icon: config.icon,
label,
allowed,
};
});
}
function createNewLicenseIconElement({ icon, label, allowed }) {
const element = document.createElement("span");
element.className = `lm-tooltip__license-icon-new ${allowed ? "allowed" : "denied"}`;
element.setAttribute("role", "img");
element.setAttribute("aria-label", label);
element.title = label;
element.style.setProperty("--license-icon-image", `url('${LICENSE_ICON_PATH}${icon}')`);
return element;
}
const LICENSE_ICON_STORAGE_KEY = "lm_license_icon_new_style";
// Module-level cache: null = not yet initialized
let _useNewIconsCached = null;
// Fetch the setting from the LoRA Manager backend API via the proper
// ComfyUI api helper (handles base URL, credentials, etc.).
// Stores the result in both the in-memory cache and localStorage so the
// value survives page reloads even before the API responds.
async function _fetchLicenseIconSetting() {
try {
const response = await api.fetchApi("/lm/settings");
if (response.ok) {
const data = await response.json();
const value = data.use_new_license_icons !== false;
_useNewIconsCached = value;
try { localStorage.setItem(LICENSE_ICON_STORAGE_KEY, String(value)); } catch (_) {}
}
} catch (_) {
// API not available; cached/localStorage fallback stays in place
}
}
function getUseNewLicenseIcons() {
// 1) In-memory cache hit
if (_useNewIconsCached !== null) {
return _useNewIconsCached;
}
// 2) localStorage — survives page reloads
try {
const stored = localStorage.getItem(LICENSE_ICON_STORAGE_KEY);
if (stored !== null) {
_useNewIconsCached = stored === "true";
// Refresh from API in background for next time
_fetchLicenseIconSetting();
return _useNewIconsCached;
}
} catch (_) {}
// 3) First-ever run: kick off API fetch, default to new style
_fetchLicenseIconSetting();
return true;
}
/**
* Lightweight preview tooltip that can display images or videos for different model types.
*/
@@ -101,6 +223,10 @@ export class PreviewTooltip {
ensureLmStyles();
// Pre-fetch license icon style from LM backend so the tooltip
// respects the standalone settings toggle as early as possible.
_fetchLicenseIconSetting();
this.element = document.createElement("div");
this.element.className = "lm-tooltip";
document.body.appendChild(this.element);
@@ -135,6 +261,7 @@ export class PreviewTooltip {
previewUrl: data.preview_url,
displayName: data.display_name ?? modelName,
licenseFlags: parseLicenseFlags(data.license_flags),
useNewLicenseIcons: data.use_new_license_icons,
};
}
@@ -150,7 +277,7 @@ export class PreviewTooltip {
};
}
const { previewUrl, displayName, licenseFlags } = raw;
const { previewUrl, displayName, licenseFlags, useNewLicenseIcons } = raw;
if (!previewUrl) {
throw new Error("No preview URL available");
}
@@ -161,6 +288,7 @@ export class PreviewTooltip {
? displayName
: this.displayNameFormatter(modelName),
licenseFlags: parseLicenseFlags(licenseFlags),
useNewLicenseIcons,
};
}
@@ -182,7 +310,7 @@ export class PreviewTooltip {
}
this.currentModelName = modelName;
const { previewUrl, displayName, licenseFlags } = await this.resolvePreviewData(
const { previewUrl, displayName, licenseFlags, useNewLicenseIcons } = await this.resolvePreviewData(
modelName
);
@@ -211,7 +339,7 @@ export class PreviewTooltip {
nameLabel.className = "lm-tooltip__label";
mediaContainer.appendChild(mediaElement);
this.renderLicenseOverlay(mediaContainer, licenseFlags);
this.renderLicenseOverlay(mediaContainer, licenseFlags, useNewLicenseIcons);
mediaContainer.appendChild(nameLabel);
this.element.appendChild(mediaContainer);
@@ -293,16 +421,25 @@ export class PreviewTooltip {
}
}
renderLicenseOverlay(container, licenseFlags) {
const icons = buildLicenseIconData(licenseFlags);
renderLicenseOverlay(container, licenseFlags, useNewLicenseIcons) {
const useNew = useNewLicenseIcons !== undefined ? useNewLicenseIcons : getUseNewLicenseIcons();
const icons = useNew
? buildNewLicenseIconData(licenseFlags)
: buildLicenseIconData(licenseFlags);
if (!icons.length) {
return;
}
const overlay = document.createElement("div");
overlay.className = "lm-tooltip__license-overlay";
overlay.className = useNew
? "lm-tooltip__license-overlay-new"
: "lm-tooltip__license-overlay";
icons.forEach((descriptor) => {
overlay.appendChild(createLicenseIconElement(descriptor));
overlay.appendChild(
useNew
? createNewLicenseIconElement(descriptor)
: createLicenseIconElement(descriptor)
);
});
container.appendChild(overlay);
}