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
This commit is contained in:
Will Miao
2026-06-18 21:07:44 +08:00
parent 258b2622d5
commit 9bbd26efe6
25 changed files with 415 additions and 9 deletions

View File

@@ -314,6 +314,7 @@
"downloads": "Downloads", "downloads": "Downloads",
"videoSettings": "Video-Einstellungen", "videoSettings": "Video-Einstellungen",
"layoutSettings": "Layout-Einstellungen", "layoutSettings": "Layout-Einstellungen",
"licenseIcons": "Lizenzsymbole",
"misc": "Verschiedenes", "misc": "Verschiedenes",
"backup": "Backups", "backup": "Backups",
"folderSettings": "Standard-Roots", "folderSettings": "Standard-Roots",
@@ -594,6 +595,10 @@
"label": "Früher Zugriff Updates ausblenden", "label": "Früher Zugriff Updates ausblenden",
"help": "Nur Early-Access-Updates" "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": { "misc": {
"includeTriggerWords": "Trigger Words in LoRA-Syntax einschließen", "includeTriggerWords": "Trigger Words in LoRA-Syntax einschließen",
"includeTriggerWordsHelp": "Trainierte Trigger Words beim Kopieren der LoRA-Syntax in die Zwischenablage einschließen", "includeTriggerWordsHelp": "Trainierte Trigger Words beim Kopieren der LoRA-Syntax in die Zwischenablage einschließen",

View File

@@ -314,6 +314,7 @@
"downloads": "Downloads", "downloads": "Downloads",
"videoSettings": "Video Settings", "videoSettings": "Video Settings",
"layoutSettings": "Layout Settings", "layoutSettings": "Layout Settings",
"licenseIcons": "License Icons",
"misc": "Miscellaneous", "misc": "Miscellaneous",
"backup": "Backups", "backup": "Backups",
"folderSettings": "Default Roots", "folderSettings": "Default Roots",
@@ -594,6 +595,10 @@
"label": "Hide Early Access Updates", "label": "Hide Early Access Updates",
"help": "When enabled, models with only early access updates will not show 'Update available' badge" "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": { "misc": {
"includeTriggerWords": "Include Trigger Words in LoRA Syntax", "includeTriggerWords": "Include Trigger Words in LoRA Syntax",
"includeTriggerWordsHelp": "Include trained trigger words when copying LoRA syntax to clipboard", "includeTriggerWordsHelp": "Include trained trigger words when copying LoRA syntax to clipboard",

View File

@@ -314,6 +314,7 @@
"downloads": "Descargas", "downloads": "Descargas",
"videoSettings": "Configuración de video", "videoSettings": "Configuración de video",
"layoutSettings": "Configuración de diseño", "layoutSettings": "Configuración de diseño",
"licenseIcons": "Iconos de licencia",
"misc": "Varios", "misc": "Varios",
"backup": "Copias de seguridad", "backup": "Copias de seguridad",
"folderSettings": "Raíces predeterminadas", "folderSettings": "Raíces predeterminadas",
@@ -594,6 +595,10 @@
"label": "Ocultar actualizaciones de acceso temprano", "label": "Ocultar actualizaciones de acceso temprano",
"help": "Solo 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": { "misc": {
"includeTriggerWords": "Incluir palabras clave en la sintaxis de LoRA", "includeTriggerWords": "Incluir palabras clave en la sintaxis de LoRA",
"includeTriggerWordsHelp": "Incluir palabras clave entrenadas al copiar la sintaxis de LoRA al portapapeles", "includeTriggerWordsHelp": "Incluir palabras clave entrenadas al copiar la sintaxis de LoRA al portapapeles",

View File

@@ -314,6 +314,7 @@
"downloads": "Téléchargements", "downloads": "Téléchargements",
"videoSettings": "Paramètres vidéo", "videoSettings": "Paramètres vidéo",
"layoutSettings": "Paramètres d'affichage", "layoutSettings": "Paramètres d'affichage",
"licenseIcons": "Icônes de licence",
"misc": "Divers", "misc": "Divers",
"backup": "Sauvegardes", "backup": "Sauvegardes",
"folderSettings": "Racines par défaut", "folderSettings": "Racines par défaut",
@@ -594,6 +595,10 @@
"label": "Masquer les mises à jour en accès anticipé", "label": "Masquer les mises à jour en accès anticipé",
"help": "Seulement 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": { "misc": {
"includeTriggerWords": "Inclure les mots-clés dans la syntaxe LoRA", "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", "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": "הורדות", "downloads": "הורדות",
"videoSettings": "הגדרות וידאו", "videoSettings": "הגדרות וידאו",
"layoutSettings": "הגדרות פריסה", "layoutSettings": "הגדרות פריסה",
"licenseIcons": "סמלי רישיון",
"misc": "שונות", "misc": "שונות",
"backup": "גיבויים", "backup": "גיבויים",
"folderSettings": "תיקיות ברירת מחדל", "folderSettings": "תיקיות ברירת מחדל",
@@ -594,6 +595,10 @@
"label": "הסתר עדכוני גישה מוקדמת", "label": "הסתר עדכוני גישה מוקדמת",
"help": "רק עדכוני גישה מוקדמת" "help": "רק עדכוני גישה מוקדמת"
}, },
"licenseIcons": {
"useNewStyle": "השתמש בסמלי רישיון מעודכנים",
"useNewStyleHelp": "הצג הרשאות רישיון עם מחוונים צבעוניים (סגנון חדש) או סמלי הגבלה בלבד (סגנון קלאסי). משקף את העיצוב העדכני של CivitAI."
},
"misc": { "misc": {
"includeTriggerWords": "כלול מילות טריגר בתחביר LoRA", "includeTriggerWords": "כלול מילות טריגר בתחביר LoRA",
"includeTriggerWordsHelp": "כלול מילות טריגר מאומנות בעת העתקת תחביר LoRA ללוח", "includeTriggerWordsHelp": "כלול מילות טריגר מאומנות בעת העתקת תחביר LoRA ללוח",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1272,6 +1272,14 @@ class ModelQueryHandler:
license_flags = (model_data or {}).get("license_flags") license_flags = (model_data or {}).get("license_flags")
if license_flags is not None: if license_flags is not None:
response_payload["license_flags"] = int(license_flags) 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(response_payload)
return web.json_response( return web.json_response(
{ {

View File

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

View File

@@ -72,6 +72,10 @@
margin-left: auto; margin-left: auto;
} }
.modal-header-actions .license-permissions {
margin-left: auto;
}
.license-restrictions { .license-restrictions {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -95,6 +99,41 @@
transform: translateY(-1px); 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 */
.info-grid { .info-grid {
display: grid; display: grid;

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

@@ -234,6 +234,95 @@ function renderLicenseIcons(modelData) {
</div>`; </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 * Display the model modal with the given model data
* @param {Object} model - Model data object * @param {Object} model - Model data object
@@ -264,7 +353,10 @@ export async function showModelModal(model, modelType) {
}; };
const escapedFilePathAttr = escapeAttribute(modelWithFullData.file_path || ''); const escapedFilePathAttr = escapeAttribute(modelWithFullData.file_path || '');
const escapedFolderPath = escapeHtml((modelWithFullData.file_path || '').replace(/[^/]+$/, '') || 'N/A'); 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 ? ` 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}"> <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')} <i class="fas fa-globe"></i> ${translate('modals.model.actions.viewOnCivitaiText', {}, 'View on Civitai')}

View File

@@ -1003,6 +1003,12 @@ export class SettingsManager {
this.loadDownloadBackendSettings(); this.loadDownloadBackendSettings();
this.loadProxySettings(); this.loadProxySettings();
// Set license icon style
const useNewLicenseIconsCheckbox = document.getElementById('useNewLicenseIcons');
if (useNewLicenseIconsCheckbox) {
useNewLicenseIconsCheckbox.checked = state.global.settings.use_new_license_icons !== false;
}
} }
loadDownloadBackendSettings() { loadDownloadBackendSettings() {
@@ -2947,6 +2953,10 @@ export class SettingsManager {
const showVersionOnCard = state.global.settings.show_version_on_card !== false; const showVersionOnCard = state.global.settings.show_version_on_card !== false;
document.body.classList.toggle('hide-card-version', !showVersionOnCard); 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_auto_enabled: true,
backup_retention_count: 5, backup_retention_count: 5,
strip_lora_on_copy: false, strip_lora_on_copy: false,
use_new_license_icons: true,
}); });
export function createDefaultSettings() { export function createDefaultSettings() {

View File

@@ -592,6 +592,30 @@
</div> </div>
</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 --> <!-- Miscellaneous -->
<div class="settings-subsection"> <div class="settings-subsection">
<div class="settings-subsection-header"> <div class="settings-subsection-header">

View File

@@ -101,14 +101,19 @@ vi.mock(API_FACTORY, () => ({
describe('Model modal license rendering', () => { describe('Model modal license rendering', () => {
let getModelApiClient; let getModelApiClient;
let state;
beforeEach(async () => { beforeEach(async () => {
document.body.innerHTML = ''; document.body.innerHTML = '';
({ getModelApiClient } = await import(API_FACTORY)); ({ getModelApiClient } = await import(API_FACTORY));
getModelApiClient.mockReset(); 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); const fetchModelMetadata = vi.fn().mockResolvedValue(null);
getModelApiClient.mockReturnValue({ getModelApiClient.mockReturnValue({
fetchModelMetadata, fetchModelMetadata,

View File

@@ -73,6 +73,40 @@
mask: var(--license-icon-image) center/contain no-repeat; 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 { .lm-loras-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -12,6 +12,8 @@ const LICENSE_FLAG_BITS = {
allowRelicense: 1 << 6, allowRelicense: 1 << 6,
}; };
// ── Set 1 (classic) icon definitions ──
const LICENSE_ICON_COPY = { const LICENSE_ICON_COPY = {
credit: "Creator credit required", credit: "Creator credit required",
image: "No selling generated content", 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 }, { 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) { function parseLicenseFlags(value) {
if (typeof value === "number") { if (typeof value === "number") {
return Number.isFinite(value) ? value : null; return Number.isFinite(value) ? value : null;
@@ -78,6 +125,81 @@ function createLicenseIconElement({ icon, label }) {
return element; 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. * Lightweight preview tooltip that can display images or videos for different model types.
*/ */
@@ -101,6 +223,10 @@ export class PreviewTooltip {
ensureLmStyles(); 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 = document.createElement("div");
this.element.className = "lm-tooltip"; this.element.className = "lm-tooltip";
document.body.appendChild(this.element); document.body.appendChild(this.element);
@@ -135,6 +261,7 @@ export class PreviewTooltip {
previewUrl: data.preview_url, previewUrl: data.preview_url,
displayName: data.display_name ?? modelName, displayName: data.display_name ?? modelName,
licenseFlags: parseLicenseFlags(data.license_flags), 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) { if (!previewUrl) {
throw new Error("No preview URL available"); throw new Error("No preview URL available");
} }
@@ -161,6 +288,7 @@ export class PreviewTooltip {
? displayName ? displayName
: this.displayNameFormatter(modelName), : this.displayNameFormatter(modelName),
licenseFlags: parseLicenseFlags(licenseFlags), licenseFlags: parseLicenseFlags(licenseFlags),
useNewLicenseIcons,
}; };
} }
@@ -182,7 +310,7 @@ export class PreviewTooltip {
} }
this.currentModelName = modelName; this.currentModelName = modelName;
const { previewUrl, displayName, licenseFlags } = await this.resolvePreviewData( const { previewUrl, displayName, licenseFlags, useNewLicenseIcons } = await this.resolvePreviewData(
modelName modelName
); );
@@ -211,7 +339,7 @@ export class PreviewTooltip {
nameLabel.className = "lm-tooltip__label"; nameLabel.className = "lm-tooltip__label";
mediaContainer.appendChild(mediaElement); mediaContainer.appendChild(mediaElement);
this.renderLicenseOverlay(mediaContainer, licenseFlags); this.renderLicenseOverlay(mediaContainer, licenseFlags, useNewLicenseIcons);
mediaContainer.appendChild(nameLabel); mediaContainer.appendChild(nameLabel);
this.element.appendChild(mediaContainer); this.element.appendChild(mediaContainer);
@@ -293,16 +421,25 @@ export class PreviewTooltip {
} }
} }
renderLicenseOverlay(container, licenseFlags) { renderLicenseOverlay(container, licenseFlags, useNewLicenseIcons) {
const icons = buildLicenseIconData(licenseFlags); const useNew = useNewLicenseIcons !== undefined ? useNewLicenseIcons : getUseNewLicenseIcons();
const icons = useNew
? buildNewLicenseIconData(licenseFlags)
: buildLicenseIconData(licenseFlags);
if (!icons.length) { if (!icons.length) {
return; return;
} }
const overlay = document.createElement("div"); 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) => { icons.forEach((descriptor) => {
overlay.appendChild(createLicenseIconElement(descriptor)); overlay.appendChild(
useNew
? createNewLicenseIconElement(descriptor)
: createLicenseIconElement(descriptor)
);
}); });
container.appendChild(overlay); container.appendChild(overlay);
} }