mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-05-06 16:36:45 -03:00
feat(civitai): add host preference for view links
This commit is contained in:
@@ -250,6 +250,19 @@
|
|||||||
"civitaiApiKey": "Civitai API Key",
|
"civitaiApiKey": "Civitai API Key",
|
||||||
"civitaiApiKeyPlaceholder": "Geben Sie Ihren Civitai API Key ein",
|
"civitaiApiKeyPlaceholder": "Geben Sie Ihren Civitai API Key ein",
|
||||||
"civitaiApiKeyHelp": "Wird für die Authentifizierung beim Herunterladen von Modellen von Civitai verwendet",
|
"civitaiApiKeyHelp": "Wird für die Authentifizierung beim Herunterladen von Modellen von Civitai verwendet",
|
||||||
|
"civitaiHost": {
|
||||||
|
"label": "Civitai-Host",
|
||||||
|
"help": "Wählen Sie aus, welche Civitai-Seite geöffnet wird, wenn Sie „View on Civitai“-Links verwenden.",
|
||||||
|
"options": {
|
||||||
|
"com": "civitai.com (nur SFW)",
|
||||||
|
"red": "civitai.red (uneingeschränkt)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"civitaiHostBanner": {
|
||||||
|
"title": "Civitai-Host-Einstellung verfügbar",
|
||||||
|
"content": "Civitai verwendet jetzt civitai.com für SFW-Inhalte und civitai.red für uneingeschränkte Inhalte. In den Einstellungen können Sie ändern, welche Seite standardmäßig geöffnet wird.",
|
||||||
|
"openSettings": "Einstellungen öffnen"
|
||||||
|
},
|
||||||
"openSettingsFileLocation": {
|
"openSettingsFileLocation": {
|
||||||
"label": "Einstellungsordner öffnen",
|
"label": "Einstellungsordner öffnen",
|
||||||
"tooltip": "Den Ordner mit der settings.json öffnen",
|
"tooltip": "Den Ordner mit der settings.json öffnen",
|
||||||
|
|||||||
@@ -250,6 +250,19 @@
|
|||||||
"civitaiApiKey": "Civitai API Key",
|
"civitaiApiKey": "Civitai API Key",
|
||||||
"civitaiApiKeyPlaceholder": "Enter your Civitai API key",
|
"civitaiApiKeyPlaceholder": "Enter your Civitai API key",
|
||||||
"civitaiApiKeyHelp": "Used for authentication when downloading models from Civitai",
|
"civitaiApiKeyHelp": "Used for authentication when downloading models from Civitai",
|
||||||
|
"civitaiHost": {
|
||||||
|
"label": "Civitai host",
|
||||||
|
"help": "Choose which Civitai site opens when using View on Civitai links.",
|
||||||
|
"options": {
|
||||||
|
"com": "civitai.com (SFW)",
|
||||||
|
"red": "civitai.red (unrestricted)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"civitaiHostBanner": {
|
||||||
|
"title": "Civitai host preference available",
|
||||||
|
"content": "Civitai now uses civitai.com for SFW content and civitai.red for unrestricted content. You can change which site opens by default in Settings.",
|
||||||
|
"openSettings": "Open Settings"
|
||||||
|
},
|
||||||
"openSettingsFileLocation": {
|
"openSettingsFileLocation": {
|
||||||
"label": "Open settings folder",
|
"label": "Open settings folder",
|
||||||
"tooltip": "Open folder containing settings.json",
|
"tooltip": "Open folder containing settings.json",
|
||||||
|
|||||||
@@ -250,6 +250,19 @@
|
|||||||
"civitaiApiKey": "Clave API de Civitai",
|
"civitaiApiKey": "Clave API de Civitai",
|
||||||
"civitaiApiKeyPlaceholder": "Introduce tu clave API de Civitai",
|
"civitaiApiKeyPlaceholder": "Introduce tu clave API de Civitai",
|
||||||
"civitaiApiKeyHelp": "Utilizada para autenticación al descargar modelos de Civitai",
|
"civitaiApiKeyHelp": "Utilizada para autenticación al descargar modelos de Civitai",
|
||||||
|
"civitaiHost": {
|
||||||
|
"label": "Host de Civitai",
|
||||||
|
"help": "Elige qué sitio de Civitai se abre al usar los enlaces de \"View on Civitai\".",
|
||||||
|
"options": {
|
||||||
|
"com": "civitai.com (solo SFW)",
|
||||||
|
"red": "civitai.red (sin restricciones)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"civitaiHostBanner": {
|
||||||
|
"title": "Preferencia de host de Civitai disponible",
|
||||||
|
"content": "Civitai ahora usa civitai.com para contenido SFW y civitai.red para contenido sin restricciones. Puedes cambiar en Ajustes qué sitio se abre por defecto.",
|
||||||
|
"openSettings": "Abrir ajustes"
|
||||||
|
},
|
||||||
"openSettingsFileLocation": {
|
"openSettingsFileLocation": {
|
||||||
"label": "Abrir carpeta de ajustes",
|
"label": "Abrir carpeta de ajustes",
|
||||||
"tooltip": "Abrir la carpeta que contiene settings.json",
|
"tooltip": "Abrir la carpeta que contiene settings.json",
|
||||||
|
|||||||
@@ -250,6 +250,19 @@
|
|||||||
"civitaiApiKey": "Clé API Civitai",
|
"civitaiApiKey": "Clé API Civitai",
|
||||||
"civitaiApiKeyPlaceholder": "Entrez votre clé API Civitai",
|
"civitaiApiKeyPlaceholder": "Entrez votre clé API Civitai",
|
||||||
"civitaiApiKeyHelp": "Utilisée pour l'authentification lors du téléchargement de modèles depuis Civitai",
|
"civitaiApiKeyHelp": "Utilisée pour l'authentification lors du téléchargement de modèles depuis Civitai",
|
||||||
|
"civitaiHost": {
|
||||||
|
"label": "Hôte Civitai",
|
||||||
|
"help": "Choisissez quel site Civitai s'ouvre lorsque vous utilisez les liens « View on Civitai ».",
|
||||||
|
"options": {
|
||||||
|
"com": "civitai.com (SFW uniquement)",
|
||||||
|
"red": "civitai.red (sans restriction)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"civitaiHostBanner": {
|
||||||
|
"title": "Préférence d’hôte Civitai disponible",
|
||||||
|
"content": "Civitai utilise désormais civitai.com pour le contenu SFW et civitai.red pour le contenu sans restriction. Vous pouvez modifier dans les paramètres le site ouvert par défaut.",
|
||||||
|
"openSettings": "Ouvrir les paramètres"
|
||||||
|
},
|
||||||
"openSettingsFileLocation": {
|
"openSettingsFileLocation": {
|
||||||
"label": "Ouvrir le dossier des paramètres",
|
"label": "Ouvrir le dossier des paramètres",
|
||||||
"tooltip": "Ouvrir le dossier contenant settings.json",
|
"tooltip": "Ouvrir le dossier contenant settings.json",
|
||||||
|
|||||||
@@ -250,6 +250,19 @@
|
|||||||
"civitaiApiKey": "מפתח API של Civitai",
|
"civitaiApiKey": "מפתח API של Civitai",
|
||||||
"civitaiApiKeyPlaceholder": "הזן את מפתח ה-API שלך מ-Civitai",
|
"civitaiApiKeyPlaceholder": "הזן את מפתח ה-API שלך מ-Civitai",
|
||||||
"civitaiApiKeyHelp": "משמש לאימות בעת הורדת מודלים מ-Civitai",
|
"civitaiApiKeyHelp": "משמש לאימות בעת הורדת מודלים מ-Civitai",
|
||||||
|
"civitaiHost": {
|
||||||
|
"label": "מארח Civitai",
|
||||||
|
"help": "בחר איזה אתר של Civitai ייפתח בעת שימוש בקישורי \"View on Civitai\".",
|
||||||
|
"options": {
|
||||||
|
"com": "civitai.com (SFW בלבד)",
|
||||||
|
"red": "civitai.red (ללא הגבלות)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"civitaiHostBanner": {
|
||||||
|
"title": "העדפת מארח Civitai זמינה",
|
||||||
|
"content": "Civitai משתמש כעת ב-civitai.com עבור תוכן SFW וב-civitai.red עבור תוכן ללא הגבלות. ניתן לשנות בהגדרות איזה אתר ייפתח כברירת מחדל.",
|
||||||
|
"openSettings": "פתח הגדרות"
|
||||||
|
},
|
||||||
"openSettingsFileLocation": {
|
"openSettingsFileLocation": {
|
||||||
"label": "פתח תיקיית הגדרות",
|
"label": "פתח תיקיית הגדרות",
|
||||||
"tooltip": "פתח את התיקייה שמכילה את settings.json",
|
"tooltip": "פתח את התיקייה שמכילה את settings.json",
|
||||||
|
|||||||
@@ -250,6 +250,19 @@
|
|||||||
"civitaiApiKey": "Civitai APIキー",
|
"civitaiApiKey": "Civitai APIキー",
|
||||||
"civitaiApiKeyPlaceholder": "Civitai APIキーを入力してください",
|
"civitaiApiKeyPlaceholder": "Civitai APIキーを入力してください",
|
||||||
"civitaiApiKeyHelp": "Civitaiからモデルをダウンロードするときの認証に使用されます",
|
"civitaiApiKeyHelp": "Civitaiからモデルをダウンロードするときの認証に使用されます",
|
||||||
|
"civitaiHost": {
|
||||||
|
"label": "Civitai ホスト",
|
||||||
|
"help": "「View on Civitai」リンクを使うときに開く Civitai サイトを選択します。",
|
||||||
|
"options": {
|
||||||
|
"com": "civitai.com(SFW のみ)",
|
||||||
|
"red": "civitai.red(制限なし)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"civitaiHostBanner": {
|
||||||
|
"title": "Civitai ホスト設定を利用できます",
|
||||||
|
"content": "Civitai は現在、SFW コンテンツには civitai.com、制限なしコンテンツには civitai.red を使用しています。設定で既定で開くサイトを変更できます。",
|
||||||
|
"openSettings": "設定を開く"
|
||||||
|
},
|
||||||
"openSettingsFileLocation": {
|
"openSettingsFileLocation": {
|
||||||
"label": "設定フォルダーを開く",
|
"label": "設定フォルダーを開く",
|
||||||
"tooltip": "settings.json を含むフォルダーを開きます",
|
"tooltip": "settings.json を含むフォルダーを開きます",
|
||||||
|
|||||||
@@ -250,6 +250,19 @@
|
|||||||
"civitaiApiKey": "Civitai API 키",
|
"civitaiApiKey": "Civitai API 키",
|
||||||
"civitaiApiKeyPlaceholder": "Civitai API 키를 입력하세요",
|
"civitaiApiKeyPlaceholder": "Civitai API 키를 입력하세요",
|
||||||
"civitaiApiKeyHelp": "Civitai에서 모델을 다운로드할 때 인증에 사용됩니다",
|
"civitaiApiKeyHelp": "Civitai에서 모델을 다운로드할 때 인증에 사용됩니다",
|
||||||
|
"civitaiHost": {
|
||||||
|
"label": "Civitai 호스트",
|
||||||
|
"help": "\"View on Civitai\" 링크를 사용할 때 어떤 Civitai 사이트를 열지 선택합니다.",
|
||||||
|
"options": {
|
||||||
|
"com": "civitai.com(SFW 전용)",
|
||||||
|
"red": "civitai.red(무제한)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"civitaiHostBanner": {
|
||||||
|
"title": "Civitai 호스트 기본 설정 사용 가능",
|
||||||
|
"content": "이제 Civitai는 SFW 콘텐츠에 civitai.com을, 무제한 콘텐츠에 civitai.red를 사용합니다. 설정에서 기본으로 열 사이트를 변경할 수 있습니다.",
|
||||||
|
"openSettings": "설정 열기"
|
||||||
|
},
|
||||||
"openSettingsFileLocation": {
|
"openSettingsFileLocation": {
|
||||||
"label": "설정 폴더 열기",
|
"label": "설정 폴더 열기",
|
||||||
"tooltip": "settings.json이 있는 폴더를 엽니다",
|
"tooltip": "settings.json이 있는 폴더를 엽니다",
|
||||||
|
|||||||
@@ -250,6 +250,19 @@
|
|||||||
"civitaiApiKey": "Ключ API Civitai",
|
"civitaiApiKey": "Ключ API Civitai",
|
||||||
"civitaiApiKeyPlaceholder": "Введите ваш ключ API Civitai",
|
"civitaiApiKeyPlaceholder": "Введите ваш ключ API Civitai",
|
||||||
"civitaiApiKeyHelp": "Используется для аутентификации при загрузке моделей с Civitai",
|
"civitaiApiKeyHelp": "Используется для аутентификации при загрузке моделей с Civitai",
|
||||||
|
"civitaiHost": {
|
||||||
|
"label": "Хост Civitai",
|
||||||
|
"help": "Выберите, какой сайт Civitai будет открываться при использовании ссылок «View on Civitai».",
|
||||||
|
"options": {
|
||||||
|
"com": "civitai.com (только SFW)",
|
||||||
|
"red": "civitai.red (без ограничений)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"civitaiHostBanner": {
|
||||||
|
"title": "Доступна настройка хоста Civitai",
|
||||||
|
"content": "Теперь Civitai использует civitai.com для контента SFW и civitai.red для контента без ограничений. В настройках можно изменить, какой сайт открывать по умолчанию.",
|
||||||
|
"openSettings": "Открыть настройки"
|
||||||
|
},
|
||||||
"openSettingsFileLocation": {
|
"openSettingsFileLocation": {
|
||||||
"label": "Открыть папку настроек",
|
"label": "Открыть папку настроек",
|
||||||
"tooltip": "Открыть папку, содержащую settings.json",
|
"tooltip": "Открыть папку, содержащую settings.json",
|
||||||
|
|||||||
@@ -250,6 +250,19 @@
|
|||||||
"civitaiApiKey": "Civitai API 密钥",
|
"civitaiApiKey": "Civitai API 密钥",
|
||||||
"civitaiApiKeyPlaceholder": "请输入你的 Civitai API 密钥",
|
"civitaiApiKeyPlaceholder": "请输入你的 Civitai API 密钥",
|
||||||
"civitaiApiKeyHelp": "用于从 Civitai 下载模型时的身份验证",
|
"civitaiApiKeyHelp": "用于从 Civitai 下载模型时的身份验证",
|
||||||
|
"civitaiHost": {
|
||||||
|
"label": "Civitai 站点",
|
||||||
|
"help": "选择使用“在 Civitai 中查看”时默认打开的 Civitai 站点。",
|
||||||
|
"options": {
|
||||||
|
"com": "civitai.com(仅 SFW)",
|
||||||
|
"red": "civitai.red(无限制)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"civitaiHostBanner": {
|
||||||
|
"title": "已提供 Civitai 站点偏好设置",
|
||||||
|
"content": "Civitai 现在使用 civitai.com 提供 SFW 内容,使用 civitai.red 提供无限制内容。你可以在设置中更改默认打开的站点。",
|
||||||
|
"openSettings": "打开设置"
|
||||||
|
},
|
||||||
"openSettingsFileLocation": {
|
"openSettingsFileLocation": {
|
||||||
"label": "打开设置文件夹",
|
"label": "打开设置文件夹",
|
||||||
"tooltip": "打开包含 settings.json 的文件夹",
|
"tooltip": "打开包含 settings.json 的文件夹",
|
||||||
|
|||||||
@@ -250,6 +250,19 @@
|
|||||||
"civitaiApiKey": "Civitai API 金鑰",
|
"civitaiApiKey": "Civitai API 金鑰",
|
||||||
"civitaiApiKeyPlaceholder": "請輸入您的 Civitai API 金鑰",
|
"civitaiApiKeyPlaceholder": "請輸入您的 Civitai API 金鑰",
|
||||||
"civitaiApiKeyHelp": "用於從 Civitai 下載模型時的身份驗證",
|
"civitaiApiKeyHelp": "用於從 Civitai 下載模型時的身份驗證",
|
||||||
|
"civitaiHost": {
|
||||||
|
"label": "Civitai 站點",
|
||||||
|
"help": "選擇使用「在 Civitai 中查看」時預設開啟的 Civitai 站點。",
|
||||||
|
"options": {
|
||||||
|
"com": "civitai.com(僅 SFW)",
|
||||||
|
"red": "civitai.red(無限制)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"civitaiHostBanner": {
|
||||||
|
"title": "已提供 Civitai 站點偏好設定",
|
||||||
|
"content": "Civitai 現在使用 civitai.com 提供 SFW 內容,使用 civitai.red 提供無限制內容。你可以在設定中變更預設開啟的站點。",
|
||||||
|
"openSettings": "開啟設定"
|
||||||
|
},
|
||||||
"openSettingsFileLocation": {
|
"openSettingsFileLocation": {
|
||||||
"label": "開啟設定資料夾",
|
"label": "開啟設定資料夾",
|
||||||
"tooltip": "開啟包含 settings.json 的資料夾",
|
"tooltip": "開啟包含 settings.json 的資料夾",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from .model_query import (
|
|||||||
resolve_sub_type,
|
resolve_sub_type,
|
||||||
)
|
)
|
||||||
from .settings_manager import get_settings_manager
|
from .settings_manager import get_settings_manager
|
||||||
|
from ..utils.civitai_utils import build_civitai_model_page_url
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -774,9 +775,12 @@ class BaseModelService(ABC):
|
|||||||
version_id = civitai_data.get("id")
|
version_id = civitai_data.get("id")
|
||||||
|
|
||||||
if model_id:
|
if model_id:
|
||||||
civitai_url = f"https://civitai.com/models/{model_id}"
|
civitai_host = self.settings.get("civitai_host", "civitai.com")
|
||||||
if version_id:
|
civitai_url = build_civitai_model_page_url(
|
||||||
civitai_url += f"?modelVersionId={version_id}"
|
model_id,
|
||||||
|
version_id,
|
||||||
|
host=civitai_host,
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"civitai_url": civitai_url,
|
"civitai_url": civitai_url,
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ DEFAULT_KEYS_CLEANUP_THRESHOLD = 10
|
|||||||
|
|
||||||
DEFAULT_SETTINGS: Dict[str, Any] = {
|
DEFAULT_SETTINGS: Dict[str, Any] = {
|
||||||
"civitai_api_key": "",
|
"civitai_api_key": "",
|
||||||
|
"civitai_host": "civitai.com",
|
||||||
"use_portable_settings": False,
|
"use_portable_settings": False,
|
||||||
"hash_chunk_size_mb": DEFAULT_HASH_CHUNK_SIZE_MB,
|
"hash_chunk_size_mb": DEFAULT_HASH_CHUNK_SIZE_MB,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from urllib.parse import parse_qs, urlparse, urlunparse
|
|||||||
|
|
||||||
|
|
||||||
_SUPPORTED_CIVITAI_PAGE_HOSTS = frozenset({"civitai.com", "civitai.red"})
|
_SUPPORTED_CIVITAI_PAGE_HOSTS = frozenset({"civitai.com", "civitai.red"})
|
||||||
|
DEFAULT_CIVITAI_PAGE_HOST = "civitai.com"
|
||||||
_DEFAULT_ALLOW_COMMERCIAL_USE: Sequence[str] = ("Sell",)
|
_DEFAULT_ALLOW_COMMERCIAL_USE: Sequence[str] = ("Sell",)
|
||||||
_LICENSE_DEFAULTS: Dict[str, Any] = {
|
_LICENSE_DEFAULTS: Dict[str, Any] = {
|
||||||
"allowNoCredit": True,
|
"allowNoCredit": True,
|
||||||
@@ -27,6 +28,44 @@ def is_supported_civitai_page_host(hostname: str | None) -> bool:
|
|||||||
return hostname.lower() in _SUPPORTED_CIVITAI_PAGE_HOSTS
|
return hostname.lower() in _SUPPORTED_CIVITAI_PAGE_HOSTS
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_civitai_page_host(hostname: str | None) -> str:
|
||||||
|
"""Return a supported Civitai page host or the default host."""
|
||||||
|
|
||||||
|
if not isinstance(hostname, str):
|
||||||
|
return DEFAULT_CIVITAI_PAGE_HOST
|
||||||
|
|
||||||
|
normalized = hostname.strip().lower()
|
||||||
|
if is_supported_civitai_page_host(normalized):
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
return DEFAULT_CIVITAI_PAGE_HOST
|
||||||
|
|
||||||
|
|
||||||
|
def build_civitai_model_page_url(
|
||||||
|
model_id: str | int | None,
|
||||||
|
version_id: str | int | None = None,
|
||||||
|
*,
|
||||||
|
host: str | None = None,
|
||||||
|
) -> str | None:
|
||||||
|
"""Build a Civitai model or model-version page URL."""
|
||||||
|
|
||||||
|
normalized_host = normalize_civitai_page_host(host)
|
||||||
|
normalized_model_id = str(model_id).strip() if model_id is not None else ""
|
||||||
|
normalized_version_id = str(version_id).strip() if version_id is not None else ""
|
||||||
|
|
||||||
|
if normalized_model_id:
|
||||||
|
path = f"/models/{normalized_model_id}"
|
||||||
|
query = f"modelVersionId={normalized_version_id}" if normalized_version_id else ""
|
||||||
|
return urlunparse(("https", normalized_host, path, "", query, ""))
|
||||||
|
|
||||||
|
if normalized_version_id:
|
||||||
|
return urlunparse(
|
||||||
|
("https", normalized_host, f"/model-versions/{normalized_version_id}", "", "", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _parse_supported_civitai_page_url(url: str | None):
|
def _parse_supported_civitai_page_url(url: str | None):
|
||||||
if not url:
|
if not url:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// PageControls.js - Manages controls for both LoRAs and Checkpoints pages
|
// PageControls.js - Manages controls for both LoRAs and Checkpoints pages
|
||||||
import { state, getCurrentPageState, setCurrentPageType } from '../../state/index.js';
|
import { state, getCurrentPageState, setCurrentPageType } from '../../state/index.js';
|
||||||
import { getStorageItem, setStorageItem, getSessionItem, setSessionItem } from '../../utils/storageHelpers.js';
|
import { getStorageItem, setStorageItem, getSessionItem, setSessionItem } from '../../utils/storageHelpers.js';
|
||||||
import { showToast } from '../../utils/uiHelpers.js';
|
import { showToast, openCivitaiByMetadata } from '../../utils/uiHelpers.js';
|
||||||
import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js';
|
import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js';
|
||||||
import { sidebarManager } from '../SidebarManager.js';
|
import { sidebarManager } from '../SidebarManager.js';
|
||||||
|
|
||||||
@@ -354,17 +354,7 @@ export class PageControls {
|
|||||||
const civitaiId = metaData.modelId;
|
const civitaiId = metaData.modelId;
|
||||||
const versionId = metaData.id;
|
const versionId = metaData.id;
|
||||||
|
|
||||||
// Build URL
|
openCivitaiByMetadata(civitaiId, versionId, modelName);
|
||||||
if (civitaiId) {
|
|
||||||
let url = `https://civitai.com/models/${civitaiId}`;
|
|
||||||
if (versionId) {
|
|
||||||
url += `?modelVersionId=${versionId}`;
|
|
||||||
}
|
|
||||||
window.open(url, '_blank');
|
|
||||||
} else {
|
|
||||||
// If no ID, try searching by name
|
|
||||||
window.open(`https://civitai.com/models?query=${encodeURIComponent(modelName)}`, '_blank');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
import { getModelApiClient } from '../../api/modelApiFactory.js';
|
||||||
import { downloadManager } from '../../managers/DownloadManager.js';
|
import { downloadManager } from '../../managers/DownloadManager.js';
|
||||||
import { modalManager } from '../../managers/ModalManager.js';
|
import { modalManager } from '../../managers/ModalManager.js';
|
||||||
import { showToast } from '../../utils/uiHelpers.js';
|
import { openCivitaiUrl, showToast } from '../../utils/uiHelpers.js';
|
||||||
import { translate } from '../../utils/i18nHelpers.js';
|
import { translate } from '../../utils/i18nHelpers.js';
|
||||||
import { state } from '../../state/index.js';
|
import { state } from '../../state/index.js';
|
||||||
|
import { buildCivitaiModelUrl } from '../../utils/civitaiUtils.js';
|
||||||
import { formatFileSize } from './utils.js';
|
import { formatFileSize } from './utils.js';
|
||||||
|
|
||||||
const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.mkv'];
|
const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.mkv'];
|
||||||
const PREVIEW_PLACEHOLDER_URL = '/loras_static/images/no-preview.png';
|
const PREVIEW_PLACEHOLDER_URL = '/loras_static/images/no-preview.png';
|
||||||
|
|
||||||
function buildCivitaiVersionUrl(modelId, versionId) {
|
function buildCivitaiVersionUrl(modelId, versionId) {
|
||||||
if (modelId == null || versionId == null) {
|
return buildCivitaiModelUrl(
|
||||||
return null;
|
modelId,
|
||||||
}
|
versionId,
|
||||||
const normalizedModelId = String(modelId).trim();
|
state?.global?.settings?.civitai_host
|
||||||
const normalizedVersionId = String(versionId).trim();
|
);
|
||||||
if (!normalizedModelId || !normalizedVersionId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const encodedModelId = encodeURIComponent(normalizedModelId);
|
|
||||||
const encodedVersionId = encodeURIComponent(normalizedVersionId);
|
|
||||||
return `https://civitai.com/models/${encodedModelId}?modelVersionId=${encodedVersionId}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
@@ -1352,6 +1347,13 @@ export function initVersionsTab({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const row = event.target.closest('.model-version-row.is-clickable');
|
const row = event.target.closest('.model-version-row.is-clickable');
|
||||||
|
const civitaiLink = event.target.closest('.version-civitai-link');
|
||||||
|
if (civitaiLink) {
|
||||||
|
event.preventDefault();
|
||||||
|
openCivitaiUrl(civitaiLink.href);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1371,7 +1373,7 @@ export function initVersionsTab({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
window.open(targetUrl, '_blank', 'noopener,noreferrer');
|
openCivitaiUrl(targetUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for extension-triggered refresh requests
|
// Listen for extension-triggered refresh requests
|
||||||
|
|||||||
@@ -802,6 +802,11 @@ export class SettingsManager {
|
|||||||
usePortableCheckbox.checked = !!state.global.settings.use_portable_settings;
|
usePortableCheckbox.checked = !!state.global.settings.use_portable_settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const civitaiHostSelect = document.getElementById('civitaiHost');
|
||||||
|
if (civitaiHostSelect) {
|
||||||
|
civitaiHostSelect.value = state.global.settings.civitai_host || 'civitai.com';
|
||||||
|
}
|
||||||
|
|
||||||
const recipesPathInput = document.getElementById('recipesPath');
|
const recipesPathInput = document.getElementById('recipesPath');
|
||||||
if (recipesPathInput) {
|
if (recipesPathInput) {
|
||||||
recipesPathInput.value = state.global.settings.recipes_path || '';
|
recipesPathInput.value = state.global.settings.recipes_path || '';
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { DEFAULT_PATH_TEMPLATES, DEFAULT_PRIORITY_TAG_CONFIG } from '../utils/co
|
|||||||
|
|
||||||
const DEFAULT_SETTINGS_BASE = Object.freeze({
|
const DEFAULT_SETTINGS_BASE = Object.freeze({
|
||||||
civitai_api_key: '',
|
civitai_api_key: '',
|
||||||
|
civitai_host: 'civitai.com',
|
||||||
use_portable_settings: false,
|
use_portable_settings: false,
|
||||||
language: 'en',
|
language: 'en',
|
||||||
show_only_sfw: false,
|
show_only_sfw: false,
|
||||||
|
|||||||
@@ -13,11 +13,64 @@ export const OptimizationMode = {
|
|||||||
THUMBNAIL: 'thumbnail',
|
THUMBNAIL: 'thumbnail',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_CIVITAI_PAGE_HOST = 'civitai.com';
|
||||||
|
|
||||||
const SUPPORTED_CIVITAI_PAGE_HOSTS = new Set([
|
const SUPPORTED_CIVITAI_PAGE_HOSTS = new Set([
|
||||||
'civitai.com',
|
'civitai.com',
|
||||||
'civitai.red',
|
'civitai.red',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export function normalizeCivitaiPageHost(hostname) {
|
||||||
|
if (!hostname || typeof hostname !== 'string') {
|
||||||
|
return DEFAULT_CIVITAI_PAGE_HOST;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = hostname.trim().toLowerCase();
|
||||||
|
if (SUPPORTED_CIVITAI_PAGE_HOSTS.has(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_CIVITAI_PAGE_HOST;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCivitaiModelUrl(modelId, versionId = null, host = DEFAULT_CIVITAI_PAGE_HOST) {
|
||||||
|
const normalizedHost = normalizeCivitaiPageHost(host);
|
||||||
|
const normalizedModelId = modelId == null ? '' : String(modelId).trim();
|
||||||
|
const normalizedVersionId = versionId == null ? '' : String(versionId).trim();
|
||||||
|
|
||||||
|
if (normalizedModelId) {
|
||||||
|
const encodedModelId = encodeURIComponent(normalizedModelId);
|
||||||
|
let url = `https://${normalizedHost}/models/${encodedModelId}`;
|
||||||
|
if (normalizedVersionId) {
|
||||||
|
url += `?modelVersionId=${encodeURIComponent(normalizedVersionId)}`;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedVersionId) {
|
||||||
|
return `https://${normalizedHost}/model-versions/${encodeURIComponent(normalizedVersionId)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCivitaiSearchUrl(query, host = DEFAULT_CIVITAI_PAGE_HOST) {
|
||||||
|
const normalizedQuery = query == null ? '' : String(query).trim();
|
||||||
|
if (!normalizedQuery) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedHost = normalizeCivitaiPageHost(host);
|
||||||
|
return `https://${normalizedHost}/models?query=${encodeURIComponent(normalizedQuery)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCivitaiUrl({ modelId = null, versionId = null, modelName = null, host = DEFAULT_CIVITAI_PAGE_HOST } = {}) {
|
||||||
|
return (
|
||||||
|
buildCivitaiModelUrl(modelId, versionId, host)
|
||||||
|
|| buildCivitaiSearchUrl(modelName, host)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rewrite Civitai preview URLs to use optimized renditions.
|
* Rewrite Civitai preview URLs to use optimized renditions.
|
||||||
* Mirrors the backend's rewrite_preview_url() function from py/utils/civitai_utils.py
|
* Mirrors the backend's rewrite_preview_url() function from py/utils/civitai_utils.py
|
||||||
|
|||||||
@@ -3,6 +3,66 @@ import { state, getCurrentPageState } from '../state/index.js';
|
|||||||
import { getStorageItem, setStorageItem } from './storageHelpers.js';
|
import { getStorageItem, setStorageItem } from './storageHelpers.js';
|
||||||
import { NODE_TYPE_ICONS, DEFAULT_NODE_COLOR } from './constants.js';
|
import { NODE_TYPE_ICONS, DEFAULT_NODE_COLOR } from './constants.js';
|
||||||
import { eventManager } from './EventManager.js';
|
import { eventManager } from './EventManager.js';
|
||||||
|
import { bannerService } from '../managers/BannerService.js';
|
||||||
|
import { modalManager } from '../managers/ModalManager.js';
|
||||||
|
import { buildCivitaiUrl, normalizeCivitaiPageHost } from './civitaiUtils.js';
|
||||||
|
|
||||||
|
const CIVITAI_HOST_INFO_BANNER_ID = 'civitai-host-preference';
|
||||||
|
const CIVITAI_HOST_INFO_BANNER_SEEN_KEY = 'civitai_host_info_banner_seen';
|
||||||
|
|
||||||
|
function getPreferredCivitaiHost() {
|
||||||
|
return normalizeCivitaiPageHost(state?.global?.settings?.civitai_host);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRegisterCivitaiHostInfoBanner() {
|
||||||
|
if (getStorageItem(CIVITAI_HOST_INFO_BANNER_SEEN_KEY, false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setStorageItem(CIVITAI_HOST_INFO_BANNER_SEEN_KEY, true);
|
||||||
|
|
||||||
|
bannerService.registerBanner(CIVITAI_HOST_INFO_BANNER_ID, {
|
||||||
|
id: CIVITAI_HOST_INFO_BANNER_ID,
|
||||||
|
title: translate(
|
||||||
|
'settings.civitaiHostBanner.title',
|
||||||
|
{},
|
||||||
|
'Civitai host preference available'
|
||||||
|
),
|
||||||
|
content: translate(
|
||||||
|
'settings.civitaiHostBanner.content',
|
||||||
|
{},
|
||||||
|
'Civitai now uses civitai.com for SFW content and civitai.red for unrestricted content. You can change which site opens by default in Settings.'
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
text: translate('settings.civitaiHostBanner.openSettings', {}, 'Open Settings'),
|
||||||
|
icon: 'fas fa-cog',
|
||||||
|
action: 'open-settings-modal',
|
||||||
|
type: 'primary',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dismissible: true,
|
||||||
|
priority: 70,
|
||||||
|
onRegister: (bannerElement) => {
|
||||||
|
const button = bannerElement.querySelector('.banner-action[data-action="open-settings-modal"]');
|
||||||
|
if (button) {
|
||||||
|
button.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
modalManager.showModal('settingsModal');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openCivitaiUrl(url) {
|
||||||
|
if (!url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
maybeRegisterCivitaiHostInfoBanner();
|
||||||
|
return window.open(url, '_blank', 'noopener,noreferrer');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility function to copy text to clipboard with fallback for older browsers
|
* Utility function to copy text to clipboard with fallback for older browsers
|
||||||
@@ -184,14 +244,15 @@ function filterByFolder(folderPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function openCivitaiByMetadata(civitaiId, versionId, modelName = null) {
|
export function openCivitaiByMetadata(civitaiId, versionId, modelName = null) {
|
||||||
if (versionId) {
|
const url = buildCivitaiUrl({
|
||||||
// Use model-versions endpoint which auto-redirects to correct model page
|
modelId: civitaiId,
|
||||||
window.open(`https://civitai.com/model-versions/${versionId}`, '_blank');
|
versionId,
|
||||||
} else if (civitaiId) {
|
modelName,
|
||||||
window.open(`https://civitai.com/models/${civitaiId}`, '_blank');
|
host: getPreferredCivitaiHost(),
|
||||||
} else if (modelName) {
|
});
|
||||||
// Fallback: search by name
|
|
||||||
window.open(`https://civitai.com/models?query=${encodeURIComponent(modelName)}`, '_blank');
|
if (url) {
|
||||||
|
openCivitaiUrl(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -114,6 +114,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-info">
|
||||||
|
<label for="civitaiHost">{{ t('settings.civitaiHost.label') }}</label>
|
||||||
|
<i class="fas fa-info-circle info-icon" data-tooltip="{{ t('settings.civitaiHost.help') }}"></i>
|
||||||
|
</div>
|
||||||
|
<div class="setting-control select-control">
|
||||||
|
<select id="civitaiHost" onchange="settingsManager.saveSelectSetting('civitaiHost', 'civitai_host')">
|
||||||
|
<option value="civitai.com">{{ t('settings.civitaiHost.options.com') }}</option>
|
||||||
|
<option value="civitai.red">{{ t('settings.civitaiHost.options.red') }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Backup -->
|
<!-- Backup -->
|
||||||
<div class="settings-subsection">
|
<div class="settings-subsection">
|
||||||
<div class="settings-subsection-header">
|
<div class="settings-subsection-header">
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ vi.mock(DOWNLOAD_MANAGER_MODULE, () => ({
|
|||||||
|
|
||||||
vi.mock(UI_HELPERS_MODULE, () => ({
|
vi.mock(UI_HELPERS_MODULE, () => ({
|
||||||
showToast: vi.fn(),
|
showToast: vi.fn(),
|
||||||
|
openCivitaiUrl: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const stateMock = {
|
const stateMock = {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const apiClientMock = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const showToastMock = vi.fn();
|
const showToastMock = vi.fn();
|
||||||
|
const openCivitaiByMetadataMock = vi.fn();
|
||||||
const updatePanelPositionsMock = vi.fn();
|
const updatePanelPositionsMock = vi.fn();
|
||||||
const downloadManagerMock = {
|
const downloadManagerMock = {
|
||||||
showDownloadModal: vi.fn(),
|
showDownloadModal: vi.fn(),
|
||||||
@@ -40,6 +41,7 @@ vi.mock('../../../static/js/api/modelApiFactory.js', () => ({
|
|||||||
|
|
||||||
vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
|
vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
|
||||||
showToast: showToastMock,
|
showToast: showToastMock,
|
||||||
|
openCivitaiByMetadata: openCivitaiByMetadataMock,
|
||||||
updatePanelPositions: updatePanelPositionsMock,
|
updatePanelPositions: updatePanelPositionsMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const getCurrentPageStateMock = vi.fn();
|
|||||||
const getSessionItemMock = vi.fn();
|
const getSessionItemMock = vi.fn();
|
||||||
const removeSessionItemMock = vi.fn();
|
const removeSessionItemMock = vi.fn();
|
||||||
const getStorageItemMock = vi.fn();
|
const getStorageItemMock = vi.fn();
|
||||||
|
const setStorageItemMock = vi.fn();
|
||||||
|
const removeStorageItemMock = vi.fn();
|
||||||
const RecipeContextMenuMock = vi.fn();
|
const RecipeContextMenuMock = vi.fn();
|
||||||
const refreshVirtualScrollMock = vi.fn();
|
const refreshVirtualScrollMock = vi.fn();
|
||||||
const refreshRecipesMock = vi.fn();
|
const refreshRecipesMock = vi.fn();
|
||||||
@@ -53,6 +55,8 @@ vi.mock('../../../static/js/utils/storageHelpers.js', () => ({
|
|||||||
getSessionItem: getSessionItemMock,
|
getSessionItem: getSessionItemMock,
|
||||||
removeSessionItem: removeSessionItemMock,
|
removeSessionItem: removeSessionItemMock,
|
||||||
getStorageItem: getStorageItemMock,
|
getStorageItem: getStorageItemMock,
|
||||||
|
setStorageItem: setStorageItemMock,
|
||||||
|
removeStorageItem: removeStorageItemMock,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../../../static/js/components/ContextMenu/index.js', () => ({
|
vi.mock('../../../static/js/components/ContextMenu/index.js', () => ({
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ describe('state module', () => {
|
|||||||
|
|
||||||
expect(defaultSettings).toMatchObject({
|
expect(defaultSettings).toMatchObject({
|
||||||
civitai_api_key: '',
|
civitai_api_key: '',
|
||||||
|
civitai_host: 'civitai.com',
|
||||||
language: 'en',
|
language: 'en',
|
||||||
blur_mature_content: true,
|
blur_mature_content: true,
|
||||||
mature_blur_level: 'R'
|
mature_blur_level: 'R'
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_CIVITAI_PAGE_HOST,
|
||||||
|
normalizeCivitaiPageHost,
|
||||||
|
buildCivitaiModelUrl,
|
||||||
|
buildCivitaiSearchUrl,
|
||||||
|
buildCivitaiUrl,
|
||||||
rewriteCivitaiUrl,
|
rewriteCivitaiUrl,
|
||||||
getOptimizedUrl,
|
getOptimizedUrl,
|
||||||
getShowcaseUrl,
|
getShowcaseUrl,
|
||||||
@@ -19,6 +24,47 @@ describe('civitaiUtils', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Civitai page URL helpers', () => {
|
||||||
|
it('normalizes invalid hosts to the default page host', () => {
|
||||||
|
expect(DEFAULT_CIVITAI_PAGE_HOST).toBe('civitai.com');
|
||||||
|
expect(normalizeCivitaiPageHost('civitai.red')).toBe('civitai.red');
|
||||||
|
expect(normalizeCivitaiPageHost(' CIVITAI.COM ')).toBe('civitai.com');
|
||||||
|
expect(normalizeCivitaiPageHost('example.com')).toBe('civitai.com');
|
||||||
|
expect(normalizeCivitaiPageHost(null)).toBe('civitai.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds model URLs using the configured host', () => {
|
||||||
|
expect(buildCivitaiModelUrl(123, 456, 'civitai.red')).toBe(
|
||||||
|
'https://civitai.red/models/123?modelVersionId=456'
|
||||||
|
);
|
||||||
|
expect(buildCivitaiModelUrl(123, null, 'civitai.com')).toBe(
|
||||||
|
'https://civitai.com/models/123'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to the model-versions endpoint when only a version id is available', () => {
|
||||||
|
expect(buildCivitaiModelUrl(null, 456, 'civitai.red')).toBe(
|
||||||
|
'https://civitai.red/model-versions/456'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds search URLs using the configured host', () => {
|
||||||
|
expect(buildCivitaiSearchUrl('demo model', 'civitai.red')).toBe(
|
||||||
|
'https://civitai.red/models?query=demo%20model'
|
||||||
|
);
|
||||||
|
expect(buildCivitaiSearchUrl('', 'civitai.red')).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('prefers model/version URLs and falls back to search URLs', () => {
|
||||||
|
expect(buildCivitaiUrl({ modelId: 321, versionId: 654, host: 'civitai.red' })).toBe(
|
||||||
|
'https://civitai.red/models/321?modelVersionId=654'
|
||||||
|
);
|
||||||
|
expect(buildCivitaiUrl({ modelName: 'search me', host: 'civitai.red' })).toBe(
|
||||||
|
'https://civitai.red/models?query=search%20me'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('rewriteCivitaiUrl', () => {
|
describe('rewriteCivitaiUrl', () => {
|
||||||
it('should rewrite image URLs with /original=true for thumbnail mode', () => {
|
it('should rewrite image URLs with /original=true for thumbnail mode', () => {
|
||||||
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';
|
const originalUrl = 'https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/abc123/original=true/12345.jpeg';
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ const {
|
|||||||
STORAGE_MODULE,
|
STORAGE_MODULE,
|
||||||
CONSTANTS_MODULE,
|
CONSTANTS_MODULE,
|
||||||
EVENT_MANAGER_MODULE,
|
EVENT_MANAGER_MODULE,
|
||||||
|
BANNER_SERVICE_MODULE,
|
||||||
|
MODAL_MANAGER_MODULE,
|
||||||
UI_HELPERS_MODULE,
|
UI_HELPERS_MODULE,
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
I18N_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname,
|
I18N_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname,
|
||||||
@@ -13,12 +15,16 @@ const {
|
|||||||
STORAGE_MODULE: new URL('../../../static/js/utils/storageHelpers.js', import.meta.url).pathname,
|
STORAGE_MODULE: new URL('../../../static/js/utils/storageHelpers.js', import.meta.url).pathname,
|
||||||
CONSTANTS_MODULE: new URL('../../../static/js/utils/constants.js', import.meta.url).pathname,
|
CONSTANTS_MODULE: new URL('../../../static/js/utils/constants.js', import.meta.url).pathname,
|
||||||
EVENT_MANAGER_MODULE: new URL('../../../static/js/utils/EventManager.js', import.meta.url).pathname,
|
EVENT_MANAGER_MODULE: new URL('../../../static/js/utils/EventManager.js', import.meta.url).pathname,
|
||||||
|
BANNER_SERVICE_MODULE: new URL('../../../static/js/managers/BannerService.js', import.meta.url).pathname,
|
||||||
|
MODAL_MANAGER_MODULE: new URL('../../../static/js/managers/ModalManager.js', import.meta.url).pathname,
|
||||||
UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname,
|
UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const translateMock = vi.fn((key, _params, fallback) => fallback || key);
|
const translateMock = vi.fn((key, _params, fallback) => fallback || key);
|
||||||
const getStorageItemMock = vi.fn();
|
const getStorageItemMock = vi.fn();
|
||||||
const setStorageItemMock = vi.fn();
|
const setStorageItemMock = vi.fn();
|
||||||
|
const registerBannerMock = vi.fn();
|
||||||
|
const showModalMock = vi.fn();
|
||||||
|
|
||||||
vi.mock(I18N_MODULE, () => ({
|
vi.mock(I18N_MODULE, () => ({
|
||||||
translate: translateMock,
|
translate: translateMock,
|
||||||
@@ -50,6 +56,18 @@ vi.mock(EVENT_MANAGER_MODULE, () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock(BANNER_SERVICE_MODULE, () => ({
|
||||||
|
bannerService: {
|
||||||
|
registerBanner: registerBannerMock,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(MODAL_MANAGER_MODULE, () => ({
|
||||||
|
modalManager: {
|
||||||
|
showModal: showModalMock,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
describe('UI helper DOM utilities', () => {
|
describe('UI helper DOM utilities', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
document.body.innerHTML = '';
|
document.body.innerHTML = '';
|
||||||
@@ -57,6 +75,8 @@ describe('UI helper DOM utilities', () => {
|
|||||||
document.documentElement.removeAttribute('data-theme');
|
document.documentElement.removeAttribute('data-theme');
|
||||||
getStorageItemMock.mockReset();
|
getStorageItemMock.mockReset();
|
||||||
setStorageItemMock.mockReset();
|
setStorageItemMock.mockReset();
|
||||||
|
registerBannerMock.mockReset();
|
||||||
|
showModalMock.mockReset();
|
||||||
translateMock.mockReset();
|
translateMock.mockReset();
|
||||||
globalThis.requestAnimationFrame = (cb) => cb();
|
globalThis.requestAnimationFrame = (cb) => cb();
|
||||||
});
|
});
|
||||||
@@ -156,4 +176,58 @@ describe('UI helper DOM utilities', () => {
|
|||||||
'#2 (Character Subgraph) Nested Loader',
|
'#2 (Character Subgraph) Nested Loader',
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('opens Civitai links using the preferred host and registers the first-use banner once', async () => {
|
||||||
|
const openSpy = vi.fn();
|
||||||
|
globalThis.window.open = openSpy;
|
||||||
|
|
||||||
|
getStorageItemMock.mockImplementation((key, defaultValue) => {
|
||||||
|
if (key === 'civitai_host_info_banner_seen') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { openCivitaiByMetadata } = await import(UI_HELPERS_MODULE);
|
||||||
|
|
||||||
|
openCivitaiByMetadata(123, 456, 'Demo Model');
|
||||||
|
|
||||||
|
expect(setStorageItemMock).toHaveBeenCalledWith('civitai_host_info_banner_seen', true);
|
||||||
|
expect(registerBannerMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(openSpy).toHaveBeenCalledWith(
|
||||||
|
'https://civitai.com/models/123?modelVersionId=456',
|
||||||
|
'_blank',
|
||||||
|
'noopener,noreferrer'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the configured red host for fallback searches', async () => {
|
||||||
|
const openSpy = vi.fn();
|
||||||
|
globalThis.window.open = openSpy;
|
||||||
|
|
||||||
|
getStorageItemMock.mockImplementation((key, defaultValue) => {
|
||||||
|
if (key === 'civitai_host_info_banner_seen') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
});
|
||||||
|
|
||||||
|
const stateModule = await import(STATE_MODULE);
|
||||||
|
stateModule.state.global = {
|
||||||
|
settings: {
|
||||||
|
civitai_host: 'civitai.red',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { openCivitaiByMetadata } = await import(UI_HELPERS_MODULE);
|
||||||
|
|
||||||
|
openCivitaiByMetadata(null, null, 'Demo Model');
|
||||||
|
|
||||||
|
expect(registerBannerMock).not.toHaveBeenCalled();
|
||||||
|
expect(openSpy).toHaveBeenCalledWith(
|
||||||
|
'https://civitai.red/models?query=Demo%20Model',
|
||||||
|
'_blank',
|
||||||
|
'noopener,noreferrer'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -886,3 +886,111 @@ async def test_format_response_defaults_update_flag_false(service_cls, extra_fie
|
|||||||
|
|
||||||
assert "update_available" in formatted
|
assert "update_available" in formatted
|
||||||
assert formatted["update_available"] is False
|
assert formatted["update_available"] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_model_civitai_url_uses_default_host():
|
||||||
|
raw_data = [
|
||||||
|
{
|
||||||
|
"file_name": "demo.safetensors",
|
||||||
|
"civitai": {"modelId": 123, "id": 456},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
class CacheStub:
|
||||||
|
def __init__(self, raw_data):
|
||||||
|
self.raw_data = raw_data
|
||||||
|
|
||||||
|
class ScannerStub:
|
||||||
|
def __init__(self, cache):
|
||||||
|
self._cache = cache
|
||||||
|
|
||||||
|
async def get_cached_data(self, *_, **__):
|
||||||
|
return self._cache
|
||||||
|
|
||||||
|
service = DummyService(
|
||||||
|
model_type="stub",
|
||||||
|
scanner=ScannerStub(CacheStub(raw_data)),
|
||||||
|
metadata_class=BaseModelMetadata,
|
||||||
|
settings_provider=StubSettings({}),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await service.get_model_civitai_url("demo.safetensors")
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"civitai_url": "https://civitai.com/models/123?modelVersionId=456",
|
||||||
|
"model_id": "123",
|
||||||
|
"version_id": "456",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_model_civitai_url_uses_configured_host():
|
||||||
|
raw_data = [
|
||||||
|
{
|
||||||
|
"file_name": "demo.safetensors",
|
||||||
|
"civitai": {"modelId": 123, "id": 456},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
class CacheStub:
|
||||||
|
def __init__(self, raw_data):
|
||||||
|
self.raw_data = raw_data
|
||||||
|
|
||||||
|
class ScannerStub:
|
||||||
|
def __init__(self, cache):
|
||||||
|
self._cache = cache
|
||||||
|
|
||||||
|
async def get_cached_data(self, *_, **__):
|
||||||
|
return self._cache
|
||||||
|
|
||||||
|
service = DummyService(
|
||||||
|
model_type="stub",
|
||||||
|
scanner=ScannerStub(CacheStub(raw_data)),
|
||||||
|
metadata_class=BaseModelMetadata,
|
||||||
|
settings_provider=StubSettings({"civitai_host": "civitai.red"}),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await service.get_model_civitai_url("demo.safetensors")
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"civitai_url": "https://civitai.red/models/123?modelVersionId=456",
|
||||||
|
"model_id": "123",
|
||||||
|
"version_id": "456",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_get_model_civitai_url_falls_back_when_host_setting_is_not_a_string():
|
||||||
|
raw_data = [
|
||||||
|
{
|
||||||
|
"file_name": "demo.safetensors",
|
||||||
|
"civitai": {"modelId": 123, "id": 456},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
class CacheStub:
|
||||||
|
def __init__(self, raw_data):
|
||||||
|
self.raw_data = raw_data
|
||||||
|
|
||||||
|
class ScannerStub:
|
||||||
|
def __init__(self, cache):
|
||||||
|
self._cache = cache
|
||||||
|
|
||||||
|
async def get_cached_data(self, *_, **__):
|
||||||
|
return self._cache
|
||||||
|
|
||||||
|
service = DummyService(
|
||||||
|
model_type="stub",
|
||||||
|
scanner=ScannerStub(CacheStub(raw_data)),
|
||||||
|
metadata_class=BaseModelMetadata,
|
||||||
|
settings_provider=StubSettings({"civitai_host": True}),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await service.get_model_civitai_url("demo.safetensors")
|
||||||
|
|
||||||
|
assert result == {
|
||||||
|
"civitai_url": "https://civitai.com/models/123?modelVersionId=456",
|
||||||
|
"model_id": "123",
|
||||||
|
"version_id": "456",
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user