From 9bbd26efe64677ec13756e49c2a138eb9a30562f Mon Sep 17 00:00:00 2001 From: Will Miao Date: Thu, 18 Jun 2026 21:07:44 +0800 Subject: [PATCH] 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 --- locales/de.json | 5 + locales/en.json | 5 + locales/es.json | 5 + locales/fr.json | 5 + locales/he.json | 5 + locales/ja.json | 5 + locales/ko.json | 5 + locales/ru.json | 5 + locales/zh-CN.json | 5 + locales/zh-TW.json | 5 + py/routes/handlers/model_handlers.py | 8 + py/services/settings_manager.py | 1 + .../css/components/lora-modal/lora-modal.css | 39 +++++ static/images/tabler/brush.svg | 1 + static/images/tabler/currency-dollar.svg | 1 + static/images/tabler/git-merge.svg | 1 + static/images/tabler/license.svg | 1 + static/images/tabler/user.svg | 1 + static/js/components/shared/ModelModal.js | 94 ++++++++++- static/js/managers/SettingsManager.js | 10 ++ static/js/state/index.js | 1 + .../components/modals/settings_modal.html | 24 +++ .../modelModal.licenseIcons.test.js | 7 +- web/comfyui/lm_styles.css | 34 ++++ web/comfyui/preview_tooltip.js | 151 +++++++++++++++++- 25 files changed, 415 insertions(+), 9 deletions(-) create mode 100644 static/images/tabler/brush.svg create mode 100644 static/images/tabler/currency-dollar.svg create mode 100644 static/images/tabler/git-merge.svg create mode 100644 static/images/tabler/license.svg create mode 100644 static/images/tabler/user.svg diff --git a/locales/de.json b/locales/de.json index daad230e..e0b905e5 100644 --- a/locales/de.json +++ b/locales/de.json @@ -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", diff --git a/locales/en.json b/locales/en.json index b4c52e2f..9d47c8f1 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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", diff --git a/locales/es.json b/locales/es.json index 11de9284..8883c1a7 100644 --- a/locales/es.json +++ b/locales/es.json @@ -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", diff --git a/locales/fr.json b/locales/fr.json index 8bd3035a..2f6d6290 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -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", diff --git a/locales/he.json b/locales/he.json index e6da1428..74f230e7 100644 --- a/locales/he.json +++ b/locales/he.json @@ -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 ללוח", diff --git a/locales/ja.json b/locales/ja.json index 9012ac58..b6384dd8 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -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構文をクリップボードにコピーする際、学習済みトリガーワードを含めます", diff --git a/locales/ko.json b/locales/ko.json index d5fdbe33..25f54734 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -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 문법을 클립보드에 복사할 때 학습된 트리거 단어를 포함합니다", diff --git a/locales/ru.json b/locales/ru.json index 7729b395..caab1fb6 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -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 в буфер обмена", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index a08697c2..40ce0a5e 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -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 语法到剪贴板时包含训练触发词", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index d1550428..556d2c9b 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -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 語法到剪貼簿時包含訓練觸發詞", diff --git a/py/routes/handlers/model_handlers.py b/py/routes/handlers/model_handlers.py index 89c64c8e..4812b858 100644 --- a/py/routes/handlers/model_handlers.py +++ b/py/routes/handlers/model_handlers.py @@ -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( { diff --git a/py/services/settings_manager.py b/py/services/settings_manager.py index 4a697f14..a7ad599c 100644 --- a/py/services/settings_manager.py +++ b/py/services/settings_manager.py @@ -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, } diff --git a/static/css/components/lora-modal/lora-modal.css b/static/css/components/lora-modal/lora-modal.css index 5f851e81..5296130e 100644 --- a/static/css/components/lora-modal/lora-modal.css +++ b/static/css/components/lora-modal/lora-modal.css @@ -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; diff --git a/static/images/tabler/brush.svg b/static/images/tabler/brush.svg new file mode 100644 index 00000000..369d4816 --- /dev/null +++ b/static/images/tabler/brush.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/tabler/currency-dollar.svg b/static/images/tabler/currency-dollar.svg new file mode 100644 index 00000000..0fb18a6b --- /dev/null +++ b/static/images/tabler/currency-dollar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/tabler/git-merge.svg b/static/images/tabler/git-merge.svg new file mode 100644 index 00000000..7a2db0c2 --- /dev/null +++ b/static/images/tabler/git-merge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/tabler/license.svg b/static/images/tabler/license.svg new file mode 100644 index 00000000..a24f5189 --- /dev/null +++ b/static/images/tabler/license.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/tabler/user.svg b/static/images/tabler/user.svg new file mode 100644 index 00000000..cbc61153 --- /dev/null +++ b/static/images/tabler/user.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/js/components/shared/ModelModal.js b/static/js/components/shared/ModelModal.js index 41de7b75..04493932 100644 --- a/static/js/components/shared/ModelModal.js +++ b/static/js/components/shared/ModelModal.js @@ -234,6 +234,95 @@ function renderLicenseIcons(modelData) { `; } +// ── 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 ``; +} + +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 `
+ ${icons.join('\n ')} +
`; +} + /** * 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 ? `
${translate('modals.model.actions.viewOnCivitaiText', {}, 'View on Civitai')} diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index 122ffa99..1440f95b 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -1003,6 +1003,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 +2953,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); + } } diff --git a/static/js/state/index.js b/static/js/state/index.js index 30a786c7..e26c4b1a 100644 --- a/static/js/state/index.js +++ b/static/js/state/index.js @@ -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() { diff --git a/templates/components/modals/settings_modal.html b/templates/components/modals/settings_modal.html index d0198b08..ab4db2a0 100644 --- a/templates/components/modals/settings_modal.html +++ b/templates/components/modals/settings_modal.html @@ -592,6 +592,30 @@
+ +
+
+

{{ t('settings.sections.licenseIcons') }}

+
+
+
+
+ +
+
+ +
+
+
+
+
diff --git a/tests/frontend/components/modelModal.licenseIcons.test.js b/tests/frontend/components/modelModal.licenseIcons.test.js index f3f94b5d..924d1f85 100644 --- a/tests/frontend/components/modelModal.licenseIcons.test.js +++ b/tests/frontend/components/modelModal.licenseIcons.test.js @@ -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, diff --git a/web/comfyui/lm_styles.css b/web/comfyui/lm_styles.css index b440d6d4..41e4bd92 100644 --- a/web/comfyui/lm_styles.css +++ b/web/comfyui/lm_styles.css @@ -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; diff --git a/web/comfyui/preview_tooltip.js b/web/comfyui/preview_tooltip.js index fdc211b8..c519cd42 100644 --- a/web/comfyui/preview_tooltip.js +++ b/web/comfyui/preview_tooltip.js @@ -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); }