mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 03:01:27 -03:00
feat(frontend): add Other Models page with subtype filter and badges
This commit is contained in:
@@ -149,6 +149,7 @@
|
|||||||
"copyCheckpointName": "Checkpoint-Name kopieren",
|
"copyCheckpointName": "Checkpoint-Name kopieren",
|
||||||
"copyEmbeddingName": "Embedding-Name kopieren",
|
"copyEmbeddingName": "Embedding-Name kopieren",
|
||||||
"embeddingNameCopied": "Embedding-Syntax kopiert",
|
"embeddingNameCopied": "Embedding-Syntax kopiert",
|
||||||
|
"modelNameCopied": "[TODO: Translate] Model name copied",
|
||||||
"sendCheckpointToWorkflow": "An ComfyUI senden",
|
"sendCheckpointToWorkflow": "An ComfyUI senden",
|
||||||
"sendEmbeddingToWorkflow": "An ComfyUI senden"
|
"sendEmbeddingToWorkflow": "An ComfyUI senden"
|
||||||
},
|
},
|
||||||
@@ -233,6 +234,7 @@
|
|||||||
"recipes": "Rezepte",
|
"recipes": "Rezepte",
|
||||||
"checkpoints": "Checkpoints",
|
"checkpoints": "Checkpoints",
|
||||||
"embeddings": "Embeddings",
|
"embeddings": "Embeddings",
|
||||||
|
"other": "[TODO: Translate] Other",
|
||||||
"statistics": "Statistiken"
|
"statistics": "Statistiken"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
@@ -1201,6 +1203,9 @@
|
|||||||
"embeddings": {
|
"embeddings": {
|
||||||
"title": "Embedding-Modelle"
|
"title": "Embedding-Modelle"
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Other Models"
|
||||||
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Stammverzeichnis",
|
"modelRoot": "Stammverzeichnis",
|
||||||
"collapseAll": "Alle Ordner einklappen",
|
"collapseAll": "Alle Ordner einklappen",
|
||||||
@@ -1878,6 +1883,10 @@
|
|||||||
"title": "Embedding Manager wird initialisiert",
|
"title": "Embedding Manager wird initialisiert",
|
||||||
"message": "Embedding-Cache wird gescannt und aufgebaut. Dies kann einige Minuten dauern..."
|
"message": "Embedding-Cache wird gescannt und aufgebaut. Dies kann einige Minuten dauern..."
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Initializing Other Models Manager",
|
||||||
|
"message": "[TODO: Translate] Scanning and building model cache. This may take a few minutes..."
|
||||||
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"title": "Rezept Manager wird initialisiert",
|
"title": "Rezept Manager wird initialisiert",
|
||||||
"message": "Rezepte werden geladen und verarbeitet. Dies kann einige Minuten dauern..."
|
"message": "Rezepte werden geladen und verarbeitet. Dies kann einige Minuten dauern..."
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
"copyCheckpointName": "Copy checkpoint name",
|
"copyCheckpointName": "Copy checkpoint name",
|
||||||
"copyEmbeddingName": "Copy embedding name",
|
"copyEmbeddingName": "Copy embedding name",
|
||||||
"embeddingNameCopied": "Embedding syntax copied",
|
"embeddingNameCopied": "Embedding syntax copied",
|
||||||
|
"modelNameCopied": "Model name copied",
|
||||||
"sendCheckpointToWorkflow": "Send to ComfyUI",
|
"sendCheckpointToWorkflow": "Send to ComfyUI",
|
||||||
"sendEmbeddingToWorkflow": "Send to ComfyUI"
|
"sendEmbeddingToWorkflow": "Send to ComfyUI"
|
||||||
},
|
},
|
||||||
@@ -233,6 +234,7 @@
|
|||||||
"recipes": "Recipes",
|
"recipes": "Recipes",
|
||||||
"checkpoints": "Checkpoints",
|
"checkpoints": "Checkpoints",
|
||||||
"embeddings": "Embeddings",
|
"embeddings": "Embeddings",
|
||||||
|
"other": "Other",
|
||||||
"statistics": "Stats"
|
"statistics": "Stats"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
@@ -1201,6 +1203,9 @@
|
|||||||
"embeddings": {
|
"embeddings": {
|
||||||
"title": "Embedding Models"
|
"title": "Embedding Models"
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "Other Models"
|
||||||
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Root",
|
"modelRoot": "Root",
|
||||||
"collapseAll": "Collapse All Folders",
|
"collapseAll": "Collapse All Folders",
|
||||||
@@ -1878,6 +1883,10 @@
|
|||||||
"title": "Initializing Embedding Manager",
|
"title": "Initializing Embedding Manager",
|
||||||
"message": "Scanning and building embedding cache. This may take a few minutes..."
|
"message": "Scanning and building embedding cache. This may take a few minutes..."
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "Initializing Other Models Manager",
|
||||||
|
"message": "Scanning and building model cache. This may take a few minutes..."
|
||||||
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"title": "Initializing Recipe Manager",
|
"title": "Initializing Recipe Manager",
|
||||||
"message": "Loading and processing recipes. This may take a few minutes..."
|
"message": "Loading and processing recipes. This may take a few minutes..."
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
"copyCheckpointName": "Copiar nombre del checkpoint",
|
"copyCheckpointName": "Copiar nombre del checkpoint",
|
||||||
"copyEmbeddingName": "Copiar nombre del embedding",
|
"copyEmbeddingName": "Copiar nombre del embedding",
|
||||||
"embeddingNameCopied": "Sintaxis de embedding copiada",
|
"embeddingNameCopied": "Sintaxis de embedding copiada",
|
||||||
|
"modelNameCopied": "[TODO: Translate] Model name copied",
|
||||||
"sendCheckpointToWorkflow": "Enviar a ComfyUI",
|
"sendCheckpointToWorkflow": "Enviar a ComfyUI",
|
||||||
"sendEmbeddingToWorkflow": "Enviar a ComfyUI"
|
"sendEmbeddingToWorkflow": "Enviar a ComfyUI"
|
||||||
},
|
},
|
||||||
@@ -233,6 +234,7 @@
|
|||||||
"recipes": "Recetas",
|
"recipes": "Recetas",
|
||||||
"checkpoints": "Checkpoints",
|
"checkpoints": "Checkpoints",
|
||||||
"embeddings": "Embeddings",
|
"embeddings": "Embeddings",
|
||||||
|
"other": "[TODO: Translate] Other",
|
||||||
"statistics": "Estadísticas"
|
"statistics": "Estadísticas"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
@@ -1201,6 +1203,9 @@
|
|||||||
"embeddings": {
|
"embeddings": {
|
||||||
"title": "Modelos embedding"
|
"title": "Modelos embedding"
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Other Models"
|
||||||
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Raíz",
|
"modelRoot": "Raíz",
|
||||||
"collapseAll": "Colapsar todas las carpetas",
|
"collapseAll": "Colapsar todas las carpetas",
|
||||||
@@ -1878,6 +1883,10 @@
|
|||||||
"title": "Inicializando gestor de embedding",
|
"title": "Inicializando gestor de embedding",
|
||||||
"message": "Escaneando y construyendo caché de embedding. Esto puede tomar unos minutos..."
|
"message": "Escaneando y construyendo caché de embedding. Esto puede tomar unos minutos..."
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Initializing Other Models Manager",
|
||||||
|
"message": "[TODO: Translate] Scanning and building model cache. This may take a few minutes..."
|
||||||
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"title": "Inicializando gestor de recetas",
|
"title": "Inicializando gestor de recetas",
|
||||||
"message": "Cargando y procesando recetas. Esto puede tomar unos minutos..."
|
"message": "Cargando y procesando recetas. Esto puede tomar unos minutos..."
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
"copyCheckpointName": "Copier le nom du checkpoint",
|
"copyCheckpointName": "Copier le nom du checkpoint",
|
||||||
"copyEmbeddingName": "Copier le nom de l'embedding",
|
"copyEmbeddingName": "Copier le nom de l'embedding",
|
||||||
"embeddingNameCopied": "Syntaxe dembedding copiée",
|
"embeddingNameCopied": "Syntaxe dembedding copiée",
|
||||||
|
"modelNameCopied": "[TODO: Translate] Model name copied",
|
||||||
"sendCheckpointToWorkflow": "Envoyer vers ComfyUI",
|
"sendCheckpointToWorkflow": "Envoyer vers ComfyUI",
|
||||||
"sendEmbeddingToWorkflow": "Envoyer vers ComfyUI"
|
"sendEmbeddingToWorkflow": "Envoyer vers ComfyUI"
|
||||||
},
|
},
|
||||||
@@ -233,6 +234,7 @@
|
|||||||
"recipes": "Recipes",
|
"recipes": "Recipes",
|
||||||
"checkpoints": "Checkpoints",
|
"checkpoints": "Checkpoints",
|
||||||
"embeddings": "Embeddings",
|
"embeddings": "Embeddings",
|
||||||
|
"other": "[TODO: Translate] Other",
|
||||||
"statistics": "Statistiques"
|
"statistics": "Statistiques"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
@@ -1201,6 +1203,9 @@
|
|||||||
"embeddings": {
|
"embeddings": {
|
||||||
"title": "Modèles Embedding"
|
"title": "Modèles Embedding"
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Other Models"
|
||||||
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Racine",
|
"modelRoot": "Racine",
|
||||||
"collapseAll": "Réduire tous les dossiers",
|
"collapseAll": "Réduire tous les dossiers",
|
||||||
@@ -1878,6 +1883,10 @@
|
|||||||
"title": "Initialisation du gestionnaire Embedding",
|
"title": "Initialisation du gestionnaire Embedding",
|
||||||
"message": "Scan et construction du cache embedding. Cela peut prendre quelques minutes..."
|
"message": "Scan et construction du cache embedding. Cela peut prendre quelques minutes..."
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Initializing Other Models Manager",
|
||||||
|
"message": "[TODO: Translate] Scanning and building model cache. This may take a few minutes..."
|
||||||
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"title": "Initialisation du gestionnaire de recipes",
|
"title": "Initialisation du gestionnaire de recipes",
|
||||||
"message": "Chargement et traitement des recipes. Cela peut prendre quelques minutes..."
|
"message": "Chargement et traitement des recipes. Cela peut prendre quelques minutes..."
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
"copyCheckpointName": "העתק שם Checkpoint",
|
"copyCheckpointName": "העתק שם Checkpoint",
|
||||||
"copyEmbeddingName": "העתק שם Embedding",
|
"copyEmbeddingName": "העתק שם Embedding",
|
||||||
"embeddingNameCopied": "תחביר Embedding הועתק",
|
"embeddingNameCopied": "תחביר Embedding הועתק",
|
||||||
|
"modelNameCopied": "[TODO: Translate] Model name copied",
|
||||||
"sendCheckpointToWorkflow": "שלח ל-ComfyUI",
|
"sendCheckpointToWorkflow": "שלח ל-ComfyUI",
|
||||||
"sendEmbeddingToWorkflow": "שלח ל-ComfyUI"
|
"sendEmbeddingToWorkflow": "שלח ל-ComfyUI"
|
||||||
},
|
},
|
||||||
@@ -233,6 +234,7 @@
|
|||||||
"recipes": "מתכונים",
|
"recipes": "מתכונים",
|
||||||
"checkpoints": "Checkpoints",
|
"checkpoints": "Checkpoints",
|
||||||
"embeddings": "Embeddings",
|
"embeddings": "Embeddings",
|
||||||
|
"other": "[TODO: Translate] Other",
|
||||||
"statistics": "סטטיסטיקה"
|
"statistics": "סטטיסטיקה"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
@@ -1201,6 +1203,9 @@
|
|||||||
"embeddings": {
|
"embeddings": {
|
||||||
"title": "מודלי Embedding"
|
"title": "מודלי Embedding"
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Other Models"
|
||||||
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "שורש",
|
"modelRoot": "שורש",
|
||||||
"collapseAll": "כווץ את כל התיקיות",
|
"collapseAll": "כווץ את כל התיקיות",
|
||||||
@@ -1878,6 +1883,10 @@
|
|||||||
"title": "מאתחל מנהל Embedding",
|
"title": "מאתחל מנהל Embedding",
|
||||||
"message": "סורק ובונה מטמון embedding. זה עשוי לקחת מספר דקות..."
|
"message": "סורק ובונה מטמון embedding. זה עשוי לקחת מספר דקות..."
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Initializing Other Models Manager",
|
||||||
|
"message": "[TODO: Translate] Scanning and building model cache. This may take a few minutes..."
|
||||||
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"title": "מאתחל מנהל מתכונים",
|
"title": "מאתחל מנהל מתכונים",
|
||||||
"message": "טוען ומעבד מתכונים. זה עשוי לקחת מספר דקות..."
|
"message": "טוען ומעבד מתכונים. זה עשוי לקחת מספר דקות..."
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
"copyCheckpointName": "Checkpoint名をコピー",
|
"copyCheckpointName": "Checkpoint名をコピー",
|
||||||
"copyEmbeddingName": "embedding名をコピー",
|
"copyEmbeddingName": "embedding名をコピー",
|
||||||
"embeddingNameCopied": "Embedding構文をコピーしました",
|
"embeddingNameCopied": "Embedding構文をコピーしました",
|
||||||
|
"modelNameCopied": "[TODO: Translate] Model name copied",
|
||||||
"sendCheckpointToWorkflow": "ComfyUIに送信",
|
"sendCheckpointToWorkflow": "ComfyUIに送信",
|
||||||
"sendEmbeddingToWorkflow": "ComfyUIに送信"
|
"sendEmbeddingToWorkflow": "ComfyUIに送信"
|
||||||
},
|
},
|
||||||
@@ -233,6 +234,7 @@
|
|||||||
"recipes": "レシピ",
|
"recipes": "レシピ",
|
||||||
"checkpoints": "Checkpoint",
|
"checkpoints": "Checkpoint",
|
||||||
"embeddings": "Embedding",
|
"embeddings": "Embedding",
|
||||||
|
"other": "[TODO: Translate] Other",
|
||||||
"statistics": "統計"
|
"statistics": "統計"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
@@ -1201,6 +1203,9 @@
|
|||||||
"embeddings": {
|
"embeddings": {
|
||||||
"title": "Embeddingモデル"
|
"title": "Embeddingモデル"
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Other Models"
|
||||||
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "ルート",
|
"modelRoot": "ルート",
|
||||||
"collapseAll": "すべてのフォルダを折りたたむ",
|
"collapseAll": "すべてのフォルダを折りたたむ",
|
||||||
@@ -1878,6 +1883,10 @@
|
|||||||
"title": "Embedding Managerを初期化中",
|
"title": "Embedding Managerを初期化中",
|
||||||
"message": "embeddingキャッシュをスキャンして構築中。数分かかる場合があります..."
|
"message": "embeddingキャッシュをスキャンして構築中。数分かかる場合があります..."
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Initializing Other Models Manager",
|
||||||
|
"message": "[TODO: Translate] Scanning and building model cache. This may take a few minutes..."
|
||||||
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"title": "レシピマネージャーを初期化中",
|
"title": "レシピマネージャーを初期化中",
|
||||||
"message": "レシピを読み込んで処理中。数分かかる場合があります..."
|
"message": "レシピを読み込んで処理中。数分かかる場合があります..."
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
"copyCheckpointName": "Checkpoint 이름 복사",
|
"copyCheckpointName": "Checkpoint 이름 복사",
|
||||||
"copyEmbeddingName": "Embedding 이름 복사",
|
"copyEmbeddingName": "Embedding 이름 복사",
|
||||||
"embeddingNameCopied": "Embedding 구문 복사됨",
|
"embeddingNameCopied": "Embedding 구문 복사됨",
|
||||||
|
"modelNameCopied": "[TODO: Translate] Model name copied",
|
||||||
"sendCheckpointToWorkflow": "ComfyUI로 전송",
|
"sendCheckpointToWorkflow": "ComfyUI로 전송",
|
||||||
"sendEmbeddingToWorkflow": "ComfyUI로 전송"
|
"sendEmbeddingToWorkflow": "ComfyUI로 전송"
|
||||||
},
|
},
|
||||||
@@ -233,6 +234,7 @@
|
|||||||
"recipes": "레시피",
|
"recipes": "레시피",
|
||||||
"checkpoints": "Checkpoint",
|
"checkpoints": "Checkpoint",
|
||||||
"embeddings": "Embedding",
|
"embeddings": "Embedding",
|
||||||
|
"other": "[TODO: Translate] Other",
|
||||||
"statistics": "통계"
|
"statistics": "통계"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
@@ -1201,6 +1203,9 @@
|
|||||||
"embeddings": {
|
"embeddings": {
|
||||||
"title": "Embedding 모델"
|
"title": "Embedding 모델"
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Other Models"
|
||||||
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "루트",
|
"modelRoot": "루트",
|
||||||
"collapseAll": "모든 폴더 접기",
|
"collapseAll": "모든 폴더 접기",
|
||||||
@@ -1878,6 +1883,10 @@
|
|||||||
"title": "Embedding Manager 초기화 중",
|
"title": "Embedding Manager 초기화 중",
|
||||||
"message": "Embedding 캐시를 스캔하고 구축하고 있습니다. 몇 분이 걸릴 수 있습니다..."
|
"message": "Embedding 캐시를 스캔하고 구축하고 있습니다. 몇 분이 걸릴 수 있습니다..."
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Initializing Other Models Manager",
|
||||||
|
"message": "[TODO: Translate] Scanning and building model cache. This may take a few minutes..."
|
||||||
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"title": "레시피 매니저 초기화 중",
|
"title": "레시피 매니저 초기화 중",
|
||||||
"message": "레시피를 로딩하고 처리하고 있습니다. 몇 분이 걸릴 수 있습니다..."
|
"message": "레시피를 로딩하고 처리하고 있습니다. 몇 분이 걸릴 수 있습니다..."
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
"copyCheckpointName": "Копировать имя checkpoint",
|
"copyCheckpointName": "Копировать имя checkpoint",
|
||||||
"copyEmbeddingName": "Копировать имя embedding",
|
"copyEmbeddingName": "Копировать имя embedding",
|
||||||
"embeddingNameCopied": "Синтаксис embedding скопирован",
|
"embeddingNameCopied": "Синтаксис embedding скопирован",
|
||||||
|
"modelNameCopied": "[TODO: Translate] Model name copied",
|
||||||
"sendCheckpointToWorkflow": "Отправить в ComfyUI",
|
"sendCheckpointToWorkflow": "Отправить в ComfyUI",
|
||||||
"sendEmbeddingToWorkflow": "Отправить в ComfyUI"
|
"sendEmbeddingToWorkflow": "Отправить в ComfyUI"
|
||||||
},
|
},
|
||||||
@@ -233,6 +234,7 @@
|
|||||||
"recipes": "Рецепты",
|
"recipes": "Рецепты",
|
||||||
"checkpoints": "Checkpoints",
|
"checkpoints": "Checkpoints",
|
||||||
"embeddings": "Embeddings",
|
"embeddings": "Embeddings",
|
||||||
|
"other": "[TODO: Translate] Other",
|
||||||
"statistics": "Статистика"
|
"statistics": "Статистика"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
@@ -1201,6 +1203,9 @@
|
|||||||
"embeddings": {
|
"embeddings": {
|
||||||
"title": "Модели Embedding"
|
"title": "Модели Embedding"
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Other Models"
|
||||||
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "Корень",
|
"modelRoot": "Корень",
|
||||||
"collapseAll": "Свернуть все папки",
|
"collapseAll": "Свернуть все папки",
|
||||||
@@ -1878,6 +1883,10 @@
|
|||||||
"title": "Инициализация Embedding Manager",
|
"title": "Инициализация Embedding Manager",
|
||||||
"message": "Сканирование и построение кэша embedding. Это может занять несколько минут..."
|
"message": "Сканирование и построение кэша embedding. Это может занять несколько минут..."
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Initializing Other Models Manager",
|
||||||
|
"message": "[TODO: Translate] Scanning and building model cache. This may take a few minutes..."
|
||||||
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"title": "Инициализация менеджера рецептов",
|
"title": "Инициализация менеджера рецептов",
|
||||||
"message": "Загрузка и обработка рецептов. Это может занять несколько минут..."
|
"message": "Загрузка и обработка рецептов. Это может занять несколько минут..."
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
"copyCheckpointName": "复制 Checkpoint 名称",
|
"copyCheckpointName": "复制 Checkpoint 名称",
|
||||||
"copyEmbeddingName": "复制 Embedding 名称",
|
"copyEmbeddingName": "复制 Embedding 名称",
|
||||||
"embeddingNameCopied": "已复制 Embedding 语法",
|
"embeddingNameCopied": "已复制 Embedding 语法",
|
||||||
|
"modelNameCopied": "[TODO: Translate] Model name copied",
|
||||||
"sendCheckpointToWorkflow": "发送到 ComfyUI",
|
"sendCheckpointToWorkflow": "发送到 ComfyUI",
|
||||||
"sendEmbeddingToWorkflow": "发送到 ComfyUI"
|
"sendEmbeddingToWorkflow": "发送到 ComfyUI"
|
||||||
},
|
},
|
||||||
@@ -233,6 +234,7 @@
|
|||||||
"recipes": "配方",
|
"recipes": "配方",
|
||||||
"checkpoints": "Checkpoint",
|
"checkpoints": "Checkpoint",
|
||||||
"embeddings": "Embedding",
|
"embeddings": "Embedding",
|
||||||
|
"other": "[TODO: Translate] Other",
|
||||||
"statistics": "统计"
|
"statistics": "统计"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
@@ -1201,6 +1203,9 @@
|
|||||||
"embeddings": {
|
"embeddings": {
|
||||||
"title": "Embedding 模型"
|
"title": "Embedding 模型"
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Other Models"
|
||||||
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "根目录",
|
"modelRoot": "根目录",
|
||||||
"collapseAll": "折叠所有文件夹",
|
"collapseAll": "折叠所有文件夹",
|
||||||
@@ -1878,6 +1883,10 @@
|
|||||||
"title": "初始化 Embedding 管理器",
|
"title": "初始化 Embedding 管理器",
|
||||||
"message": "正在扫描并构建 Embedding 缓存。这可能需要几分钟..."
|
"message": "正在扫描并构建 Embedding 缓存。这可能需要几分钟..."
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Initializing Other Models Manager",
|
||||||
|
"message": "[TODO: Translate] Scanning and building model cache. This may take a few minutes..."
|
||||||
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"title": "初始化配方管理器",
|
"title": "初始化配方管理器",
|
||||||
"message": "正在加载和处理配方。这可能需要几分钟..."
|
"message": "正在加载和处理配方。这可能需要几分钟..."
|
||||||
|
|||||||
@@ -149,6 +149,7 @@
|
|||||||
"copyCheckpointName": "複製 Checkpoint 名稱",
|
"copyCheckpointName": "複製 Checkpoint 名稱",
|
||||||
"copyEmbeddingName": "複製嵌入名稱",
|
"copyEmbeddingName": "複製嵌入名稱",
|
||||||
"embeddingNameCopied": "已複製 Embedding 語法",
|
"embeddingNameCopied": "已複製 Embedding 語法",
|
||||||
|
"modelNameCopied": "[TODO: Translate] Model name copied",
|
||||||
"sendCheckpointToWorkflow": "傳送到 ComfyUI",
|
"sendCheckpointToWorkflow": "傳送到 ComfyUI",
|
||||||
"sendEmbeddingToWorkflow": "傳送到 ComfyUI"
|
"sendEmbeddingToWorkflow": "傳送到 ComfyUI"
|
||||||
},
|
},
|
||||||
@@ -233,6 +234,7 @@
|
|||||||
"recipes": "配方",
|
"recipes": "配方",
|
||||||
"checkpoints": "Checkpoint",
|
"checkpoints": "Checkpoint",
|
||||||
"embeddings": "Embedding",
|
"embeddings": "Embedding",
|
||||||
|
"other": "[TODO: Translate] Other",
|
||||||
"statistics": "統計"
|
"statistics": "統計"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
@@ -1201,6 +1203,9 @@
|
|||||||
"embeddings": {
|
"embeddings": {
|
||||||
"title": "Embedding 模型"
|
"title": "Embedding 模型"
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Other Models"
|
||||||
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"modelRoot": "根目錄",
|
"modelRoot": "根目錄",
|
||||||
"collapseAll": "全部摺疊資料夾",
|
"collapseAll": "全部摺疊資料夾",
|
||||||
@@ -1878,6 +1883,10 @@
|
|||||||
"title": "初始化 Embedding 管理器",
|
"title": "初始化 Embedding 管理器",
|
||||||
"message": "正在掃描並建立 Embedding 快取,可能需要幾分鐘..."
|
"message": "正在掃描並建立 Embedding 快取,可能需要幾分鐘..."
|
||||||
},
|
},
|
||||||
|
"other": {
|
||||||
|
"title": "[TODO: Translate] Initializing Other Models Manager",
|
||||||
|
"message": "[TODO: Translate] Scanning and building model cache. This may take a few minutes..."
|
||||||
|
},
|
||||||
"recipes": {
|
"recipes": {
|
||||||
"title": "初始化配方管理器",
|
"title": "初始化配方管理器",
|
||||||
"message": "正在載入並處理配方,可能需要幾分鐘..."
|
"message": "正在載入並處理配方,可能需要幾分鐘..."
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { state } from '../state/index.js';
|
|||||||
export const MODEL_TYPES = {
|
export const MODEL_TYPES = {
|
||||||
LORA: 'loras',
|
LORA: 'loras',
|
||||||
CHECKPOINT: 'checkpoints',
|
CHECKPOINT: 'checkpoints',
|
||||||
EMBEDDING: 'embeddings' // Future model type
|
EMBEDDING: 'embeddings',
|
||||||
|
OTHER: 'other'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Base API configuration for each model type
|
// Base API configuration for each model type
|
||||||
@@ -40,6 +41,15 @@ export const MODEL_CONFIG = {
|
|||||||
supportsBulkOperations: true,
|
supportsBulkOperations: true,
|
||||||
supportsMove: true,
|
supportsMove: true,
|
||||||
templateName: 'embeddings.html'
|
templateName: 'embeddings.html'
|
||||||
|
},
|
||||||
|
[MODEL_TYPES.OTHER]: {
|
||||||
|
displayName: 'Other Model',
|
||||||
|
singularName: 'other',
|
||||||
|
defaultPageSize: 100,
|
||||||
|
supportsLetterFilter: false,
|
||||||
|
supportsBulkOperations: true,
|
||||||
|
supportsMove: true,
|
||||||
|
templateName: 'other.html'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,6 +143,9 @@ export const MODEL_SPECIFIC_ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
[MODEL_TYPES.EMBEDDING]: {
|
[MODEL_TYPES.EMBEDDING]: {
|
||||||
metadata: `/api/lm/${MODEL_TYPES.EMBEDDING}/metadata`,
|
metadata: `/api/lm/${MODEL_TYPES.EMBEDDING}/metadata`,
|
||||||
|
},
|
||||||
|
[MODEL_TYPES.OTHER]: {
|
||||||
|
metadata: `/api/lm/${MODEL_TYPES.OTHER}/metadata`,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { LoraApiClient } from './loraApi.js';
|
import { LoraApiClient } from './loraApi.js';
|
||||||
import { CheckpointApiClient } from './checkpointApi.js';
|
import { CheckpointApiClient } from './checkpointApi.js';
|
||||||
import { EmbeddingApiClient } from './embeddingApi.js';
|
import { EmbeddingApiClient } from './embeddingApi.js';
|
||||||
|
import { OtherApiClient } from './otherApi.js';
|
||||||
import { MODEL_TYPES, isValidModelType } from './apiConfig.js';
|
import { MODEL_TYPES, isValidModelType } from './apiConfig.js';
|
||||||
import { state } from '../state/index.js';
|
import { state } from '../state/index.js';
|
||||||
|
|
||||||
@@ -12,6 +13,8 @@ export function createModelApiClient(modelType) {
|
|||||||
return new CheckpointApiClient(MODEL_TYPES.CHECKPOINT);
|
return new CheckpointApiClient(MODEL_TYPES.CHECKPOINT);
|
||||||
case MODEL_TYPES.EMBEDDING:
|
case MODEL_TYPES.EMBEDDING:
|
||||||
return new EmbeddingApiClient(MODEL_TYPES.EMBEDDING);
|
return new EmbeddingApiClient(MODEL_TYPES.EMBEDDING);
|
||||||
|
case MODEL_TYPES.OTHER:
|
||||||
|
return new OtherApiClient(MODEL_TYPES.OTHER);
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported model type: ${modelType}`);
|
throw new Error(`Unsupported model type: ${modelType}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { BaseModelApiClient } from './baseModelApi.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Other-models-specific API client (VAE, upscalers, text encoders, etc.)
|
||||||
|
*/
|
||||||
|
export class OtherApiClient extends BaseModelApiClient {
|
||||||
|
}
|
||||||
@@ -139,8 +139,8 @@ export class BulkContextMenu extends BaseContextMenu {
|
|||||||
|
|
||||||
const downloadExampleImagesSubmenu = this.menu.querySelector('[data-has-submenu="download-example-images"]');
|
const downloadExampleImagesSubmenu = this.menu.querySelector('[data-has-submenu="download-example-images"]');
|
||||||
if (downloadExampleImagesSubmenu) {
|
if (downloadExampleImagesSubmenu) {
|
||||||
// Show on model pages (loras, checkpoints, embeddings), hide on recipes
|
// Show on model pages (loras, checkpoints, embeddings, other), hide on recipes
|
||||||
downloadExampleImagesSubmenu.style.display = ['loras', 'checkpoints', 'embeddings'].includes(currentModelType) ? 'flex' : 'none';
|
downloadExampleImagesSubmenu.style.display = ['loras', 'checkpoints', 'embeddings', 'other'].includes(currentModelType) ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
const skipMetadataRefreshItem = this.menu.querySelector('[data-action="skip-metadata-refresh"]');
|
const skipMetadataRefreshItem = this.menu.querySelector('[data-action="skip-metadata-refresh"]');
|
||||||
|
|||||||
@@ -112,7 +112,8 @@ export const ModelContextMenuMixin = {
|
|||||||
const prefixMap = {
|
const prefixMap = {
|
||||||
lora: 'loras',
|
lora: 'loras',
|
||||||
checkpoint: 'checkpoints',
|
checkpoint: 'checkpoints',
|
||||||
embedding: 'embeddings'
|
embedding: 'embeddings',
|
||||||
|
other: 'other'
|
||||||
};
|
};
|
||||||
return prefixMap[this.modelType] || 'loras';
|
return prefixMap[this.modelType] || 'loras';
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { BaseContextMenu } from './BaseContextMenu.js';
|
||||||
|
import { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
|
||||||
|
import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js';
|
||||||
|
import { moveManager } from '../../managers/MoveManager.js';
|
||||||
|
import { showDeleteModal, showExcludeModal } from '../../utils/modalUtils.js';
|
||||||
|
|
||||||
|
export class OtherContextMenu extends BaseContextMenu {
|
||||||
|
constructor() {
|
||||||
|
super('otherContextMenu', '.model-card');
|
||||||
|
this.nsfwSelector = document.getElementById('nsfwLevelSelector');
|
||||||
|
this.modelType = 'other';
|
||||||
|
this.resetAndReload = resetAndReload;
|
||||||
|
|
||||||
|
this.initNSFWSelector();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementation needed by the mixin
|
||||||
|
async saveModelMetadata(filePath, data) {
|
||||||
|
return getModelApiClient().saveModelMetadata(filePath, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
showMenu(x, y, card) {
|
||||||
|
super.showMenu(x, y, card);
|
||||||
|
this.updateExcludeMenuItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMenuAction(action) {
|
||||||
|
// First try to handle with common actions
|
||||||
|
if (ModelContextMenuMixin.handleCommonMenuActions.call(this, action)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiClient = getModelApiClient();
|
||||||
|
|
||||||
|
// Otherwise handle other-models-specific actions
|
||||||
|
switch(action) {
|
||||||
|
case 'details':
|
||||||
|
// Show model details
|
||||||
|
this.currentCard.click();
|
||||||
|
break;
|
||||||
|
case 'replace-preview':
|
||||||
|
// Add new action for replacing preview images
|
||||||
|
apiClient.replaceModelPreview(this.currentCard.dataset.filepath);
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
showDeleteModal(this.currentCard.dataset.filepath);
|
||||||
|
break;
|
||||||
|
case 'copyname':
|
||||||
|
// Copy model name
|
||||||
|
if (this.currentCard.querySelector('.fa-copy')) {
|
||||||
|
this.currentCard.querySelector('.fa-copy').click();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'refresh-metadata':
|
||||||
|
// Refresh metadata from CivitAI
|
||||||
|
apiClient.refreshSingleModelMetadata(this.currentCard.dataset.filepath);
|
||||||
|
break;
|
||||||
|
case 'move':
|
||||||
|
moveManager.showMoveModal(this.currentCard.dataset.filepath);
|
||||||
|
break;
|
||||||
|
case 'exclude':
|
||||||
|
showExcludeModal(this.currentCard.dataset.filepath);
|
||||||
|
break;
|
||||||
|
case 'restore':
|
||||||
|
this.restoreExcludedModel(this.currentCard.dataset.filepath);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mix in shared methods
|
||||||
|
Object.assign(OtherContextMenu.prototype, ModelContextMenuMixin);
|
||||||
@@ -2,6 +2,7 @@ export { LoraContextMenu } from './LoraContextMenu.js';
|
|||||||
export { RecipeContextMenu } from './RecipeContextMenu.js';
|
export { RecipeContextMenu } from './RecipeContextMenu.js';
|
||||||
export { CheckpointContextMenu } from './CheckpointContextMenu.js';
|
export { CheckpointContextMenu } from './CheckpointContextMenu.js';
|
||||||
export { EmbeddingContextMenu } from './EmbeddingContextMenu.js';
|
export { EmbeddingContextMenu } from './EmbeddingContextMenu.js';
|
||||||
|
export { OtherContextMenu } from './OtherContextMenu.js';
|
||||||
export { GlobalContextMenu } from './GlobalContextMenu.js';
|
export { GlobalContextMenu } from './GlobalContextMenu.js';
|
||||||
export { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
|
export { ModelContextMenuMixin } from './ModelContextMenuMixin.js';
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ import { LoraContextMenu } from './LoraContextMenu.js';
|
|||||||
import { RecipeContextMenu } from './RecipeContextMenu.js';
|
import { RecipeContextMenu } from './RecipeContextMenu.js';
|
||||||
import { CheckpointContextMenu } from './CheckpointContextMenu.js';
|
import { CheckpointContextMenu } from './CheckpointContextMenu.js';
|
||||||
import { EmbeddingContextMenu } from './EmbeddingContextMenu.js';
|
import { EmbeddingContextMenu } from './EmbeddingContextMenu.js';
|
||||||
|
import { OtherContextMenu } from './OtherContextMenu.js';
|
||||||
import { GlobalContextMenu } from './GlobalContextMenu.js';
|
import { GlobalContextMenu } from './GlobalContextMenu.js';
|
||||||
|
|
||||||
// Factory method to create page-specific context menu instances
|
// Factory method to create page-specific context menu instances
|
||||||
@@ -22,6 +24,8 @@ export function createPageContextMenu(pageType) {
|
|||||||
return new CheckpointContextMenu();
|
return new CheckpointContextMenu();
|
||||||
case 'embeddings':
|
case 'embeddings':
|
||||||
return new EmbeddingContextMenu();
|
return new EmbeddingContextMenu();
|
||||||
|
case 'other':
|
||||||
|
return new OtherContextMenu();
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export class HeaderManager {
|
|||||||
if (path.includes('/loras/recipes')) return 'recipes';
|
if (path.includes('/loras/recipes')) return 'recipes';
|
||||||
if (path.includes('/checkpoints')) return 'checkpoints';
|
if (path.includes('/checkpoints')) return 'checkpoints';
|
||||||
if (path.includes('/embeddings')) return 'embeddings';
|
if (path.includes('/embeddings')) return 'embeddings';
|
||||||
|
if (path.includes('/other')) return 'other';
|
||||||
if (path.includes('/statistics')) return 'statistics';
|
if (path.includes('/statistics')) return 'statistics';
|
||||||
if (path.includes('/loras')) return 'loras';
|
if (path.includes('/loras')) return 'loras';
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
|
|||||||
@@ -1126,6 +1126,7 @@ export class SidebarManager {
|
|||||||
recipes: 'Recipes',
|
recipes: 'Recipes',
|
||||||
checkpoints: 'Checkpoints',
|
checkpoints: 'Checkpoints',
|
||||||
embeddings: 'Embeddings',
|
embeddings: 'Embeddings',
|
||||||
|
other: 'Other Models',
|
||||||
};
|
};
|
||||||
return names[this.pageType] || this.pageType;
|
return names[this.pageType] || this.pageType;
|
||||||
}
|
}
|
||||||
@@ -1804,7 +1805,7 @@ export class SidebarManager {
|
|||||||
_migrateOldSettings() {
|
_migrateOldSettings() {
|
||||||
if (getStorageItem('_sidebar_migration_done')) return;
|
if (getStorageItem('_sidebar_migration_done')) return;
|
||||||
|
|
||||||
const PAGES = ['loras', 'recipes', 'checkpoints', 'embeddings'];
|
const PAGES = ['loras', 'recipes', 'checkpoints', 'embeddings', 'other'];
|
||||||
|
|
||||||
// 1. Migrate global hide setting to per-page
|
// 1. Migrate global hide setting to per-page
|
||||||
if (state?.global?.settings?.show_folder_sidebar === false) {
|
if (state?.global?.settings?.show_folder_sidebar === false) {
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
// OtherControls.js - Specific implementation for the Other Models page
|
||||||
|
import { PageControls } from './PageControls.js';
|
||||||
|
import { getModelApiClient, resetAndReload } from '../../api/modelApiFactory.js';
|
||||||
|
import { showToast } from '../../utils/uiHelpers.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OtherControls class - Extends PageControls for the Other Models page
|
||||||
|
* (VAE, upscalers, text encoders, CLIP vision, ControlNet, ...)
|
||||||
|
*/
|
||||||
|
export class OtherControls extends PageControls {
|
||||||
|
constructor() {
|
||||||
|
// Initialize with 'other' page type
|
||||||
|
super('other');
|
||||||
|
|
||||||
|
// Register API methods specific to the Other Models page
|
||||||
|
this.registerOtherAPI();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register Other-models-specific API methods
|
||||||
|
*/
|
||||||
|
registerOtherAPI() {
|
||||||
|
const otherAPI = {
|
||||||
|
// Core API functions
|
||||||
|
loadMoreModels: async (resetPage = false, updateFolders = false) => {
|
||||||
|
return await getModelApiClient().loadMoreWithVirtualScroll(resetPage, updateFolders);
|
||||||
|
},
|
||||||
|
|
||||||
|
resetAndReload: async (updateFolders = false) => {
|
||||||
|
return await resetAndReload(updateFolders);
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshModels: async (fullRebuild = false) => {
|
||||||
|
return await getModelApiClient().refreshModels(fullRebuild);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add fetch from Civitai functionality for other models
|
||||||
|
fetchFromCivitai: async () => {
|
||||||
|
return await getModelApiClient().fetchCivitaiMetadata();
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleBulkMode: () => {
|
||||||
|
if (window.bulkManager) {
|
||||||
|
window.bulkManager.toggleBulkMode();
|
||||||
|
} else {
|
||||||
|
console.error('Bulk manager not available');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// No clearCustomFilter implementation is needed for other models
|
||||||
|
// as custom filters are currently only used for LoRAs
|
||||||
|
clearCustomFilter: async () => {
|
||||||
|
showToast('toast.filters.noCustomFilterToClear', {}, 'info');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register the API
|
||||||
|
this.registerAPI(otherAPI);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,13 +3,14 @@ import { PageControls } from './PageControls.js';
|
|||||||
import { LorasControls } from './LorasControls.js';
|
import { LorasControls } from './LorasControls.js';
|
||||||
import { CheckpointsControls } from './CheckpointsControls.js';
|
import { CheckpointsControls } from './CheckpointsControls.js';
|
||||||
import { EmbeddingsControls } from './EmbeddingsControls.js';
|
import { EmbeddingsControls } from './EmbeddingsControls.js';
|
||||||
|
import { OtherControls } from './OtherControls.js';
|
||||||
|
|
||||||
// Export the classes
|
// Export the classes
|
||||||
export { PageControls, LorasControls, CheckpointsControls, EmbeddingsControls };
|
export { PageControls, LorasControls, CheckpointsControls, EmbeddingsControls, OtherControls };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory function to create the appropriate controls based on page type
|
* Factory function to create the appropriate controls based on page type
|
||||||
* @param {string} pageType - The type of page ('loras', 'checkpoints', or 'embeddings')
|
* @param {string} pageType - The type of page ('loras', 'checkpoints', 'embeddings', or 'other')
|
||||||
* @returns {PageControls} - The appropriate controls instance
|
* @returns {PageControls} - The appropriate controls instance
|
||||||
*/
|
*/
|
||||||
export function createPageControls(pageType) {
|
export function createPageControls(pageType) {
|
||||||
@@ -19,8 +20,10 @@ export function createPageControls(pageType) {
|
|||||||
return new CheckpointsControls();
|
return new CheckpointsControls();
|
||||||
} else if (pageType === 'embeddings') {
|
} else if (pageType === 'embeddings') {
|
||||||
return new EmbeddingsControls();
|
return new EmbeddingsControls();
|
||||||
|
} else if (pageType === 'other') {
|
||||||
|
return new OtherControls();
|
||||||
} else {
|
} else {
|
||||||
console.error(`Unknown page type: ${pageType}`);
|
console.error(`Unknown page type: ${pageType}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ class InitializationManager {
|
|||||||
this.pageType = 'recipes';
|
this.pageType = 'recipes';
|
||||||
} else if (path.includes('/checkpoints')) {
|
} else if (path.includes('/checkpoints')) {
|
||||||
this.pageType = 'checkpoints';
|
this.pageType = 'checkpoints';
|
||||||
|
} else if (path.includes('/other')) {
|
||||||
|
this.pageType = 'other';
|
||||||
} else if (path.includes('/loras')) {
|
} else if (path.includes('/loras')) {
|
||||||
this.pageType = 'loras';
|
this.pageType = 'loras';
|
||||||
} else if (path.includes('/embeddings')) {
|
} else if (path.includes('/embeddings')) {
|
||||||
@@ -221,6 +223,7 @@ class InitializationManager {
|
|||||||
'lora': 'loras',
|
'lora': 'loras',
|
||||||
'checkpoint': 'checkpoints',
|
'checkpoint': 'checkpoints',
|
||||||
'embedding': 'embeddings',
|
'embedding': 'embeddings',
|
||||||
|
'other': 'other',
|
||||||
'recipe': 'recipes'
|
'recipe': 'recipes'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -250,6 +250,11 @@ function handleCopyAction(card, modelType) {
|
|||||||
const embeddingCode = folder ? `embedding:${folder}/${name}` : `embedding:${name}`;
|
const embeddingCode = folder ? `embedding:${folder}/${name}` : `embedding:${name}`;
|
||||||
const message = translate('modelCard.actions.embeddingNameCopied', {}, 'Embedding syntax copied');
|
const message = translate('modelCard.actions.embeddingNameCopied', {}, 'Embedding syntax copied');
|
||||||
copyToClipboard(embeddingCode, message);
|
copyToClipboard(embeddingCode, message);
|
||||||
|
} else {
|
||||||
|
// Other model types (VAE, upscalers, ...) - copy the file name
|
||||||
|
const fileName = card.dataset.file_name;
|
||||||
|
const message = translate('modelCard.actions.modelNameCopied', {}, 'Model name copied');
|
||||||
|
copyToClipboard(fileName, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -116,7 +116,7 @@ export class AppCore {
|
|||||||
initializePageFeatures() {
|
initializePageFeatures() {
|
||||||
const pageType = this.getPageType();
|
const pageType = this.getPageType();
|
||||||
|
|
||||||
if (['loras', 'recipes', 'checkpoints', 'embeddings'].includes(pageType)) {
|
if (['loras', 'recipes', 'checkpoints', 'embeddings', 'other'].includes(pageType)) {
|
||||||
this.initializeContextMenus(pageType);
|
this.initializeContextMenus(pageType);
|
||||||
initializeInfiniteScroll(pageType);
|
initializeInfiniteScroll(pageType);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -424,12 +424,13 @@ class BannerService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current page type from the URL
|
* Get the current page type from the URL
|
||||||
* @returns {string} Page type (loras, checkpoints, embeddings, recipes)
|
* @returns {string} Page type (loras, checkpoints, embeddings, other, recipes)
|
||||||
*/
|
*/
|
||||||
getCurrentPageType() {
|
getCurrentPageType() {
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
if (path.includes('/checkpoints')) return 'checkpoints';
|
if (path.includes('/checkpoints')) return 'checkpoints';
|
||||||
if (path.includes('/embeddings')) return 'embeddings';
|
if (path.includes('/embeddings')) return 'embeddings';
|
||||||
|
if (path.includes('/other')) return 'other';
|
||||||
if (path.includes('/recipes')) return 'recipes';
|
if (path.includes('/recipes')) return 'recipes';
|
||||||
return 'loras';
|
return 'loras';
|
||||||
}
|
}
|
||||||
@@ -443,7 +444,8 @@ class BannerService {
|
|||||||
const endpoints = {
|
const endpoints = {
|
||||||
'loras': '/api/lm/loras/reload?rebuild=true',
|
'loras': '/api/lm/loras/reload?rebuild=true',
|
||||||
'checkpoints': '/api/lm/checkpoints/reload?rebuild=true',
|
'checkpoints': '/api/lm/checkpoints/reload?rebuild=true',
|
||||||
'embeddings': '/api/lm/embeddings/reload?rebuild=true'
|
'embeddings': '/api/lm/embeddings/reload?rebuild=true',
|
||||||
|
'other': '/api/lm/other/reload?rebuild=true'
|
||||||
};
|
};
|
||||||
return endpoints[pageType] || endpoints['loras'];
|
return endpoints[pageType] || endpoints['loras'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,20 @@ export class BulkManager {
|
|||||||
setFavorite: true,
|
setFavorite: true,
|
||||||
unfavorite: true
|
unfavorite: true
|
||||||
},
|
},
|
||||||
|
[MODEL_TYPES.OTHER]: {
|
||||||
|
addTags: true,
|
||||||
|
sendToWorkflow: false,
|
||||||
|
copyAll: false,
|
||||||
|
refreshAll: true,
|
||||||
|
checkUpdates: true,
|
||||||
|
moveAll: true,
|
||||||
|
autoOrganize: true,
|
||||||
|
deleteAll: true,
|
||||||
|
setContentRating: true,
|
||||||
|
skipMetadataRefresh: true,
|
||||||
|
setFavorite: true,
|
||||||
|
unfavorite: true
|
||||||
|
},
|
||||||
recipes: {
|
recipes: {
|
||||||
addTags: true,
|
addTags: true,
|
||||||
sendToWorkflow: false,
|
sendToWorkflow: false,
|
||||||
|
|||||||
@@ -805,7 +805,7 @@ export class FilterManager {
|
|||||||
// Call the appropriate manager's load method based on page type
|
// Call the appropriate manager's load method based on page type
|
||||||
if (this.currentPage === 'recipes' && window.recipeManager) {
|
if (this.currentPage === 'recipes' && window.recipeManager) {
|
||||||
await window.recipeManager.loadRecipes(true);
|
await window.recipeManager.loadRecipes(true);
|
||||||
} else if (this.currentPage === 'loras' || this.currentPage === 'embeddings' || this.currentPage === 'checkpoints') {
|
} else if (this.currentPage === 'loras' || this.currentPage === 'embeddings' || this.currentPage === 'checkpoints' || this.currentPage === 'other') {
|
||||||
// For models page, reset the page and reload
|
// For models page, reset the page and reload
|
||||||
await getModelApiClient().loadMoreWithVirtualScroll(true, false);
|
await getModelApiClient().loadMoreWithVirtualScroll(true, false);
|
||||||
}
|
}
|
||||||
@@ -904,7 +904,7 @@ export class FilterManager {
|
|||||||
// Reload data using the appropriate method for the current page
|
// Reload data using the appropriate method for the current page
|
||||||
if (this.currentPage === 'recipes' && window.recipeManager) {
|
if (this.currentPage === 'recipes' && window.recipeManager) {
|
||||||
await window.recipeManager.loadRecipes(true);
|
await window.recipeManager.loadRecipes(true);
|
||||||
} else if (this.currentPage === 'loras' || this.currentPage === 'checkpoints' || this.currentPage === 'embeddings') {
|
} else if (this.currentPage === 'loras' || this.currentPage === 'checkpoints' || this.currentPage === 'embeddings' || this.currentPage === 'other') {
|
||||||
await getModelApiClient().loadMoreWithVirtualScroll(true, true);
|
await getModelApiClient().loadMoreWithVirtualScroll(true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ class MoveManager {
|
|||||||
this.bulkFilePaths = null;
|
this.bulkFilePaths = null;
|
||||||
|
|
||||||
const apiClient = this._getApiClient(modelType);
|
const apiClient = this._getApiClient(modelType);
|
||||||
const currentPageType = state.currentPageType;
|
|
||||||
const modelConfig = apiClient.apiConfig.config;
|
const modelConfig = apiClient.apiConfig.config;
|
||||||
|
|
||||||
// Handle bulk mode
|
// Handle bulk mode
|
||||||
@@ -113,7 +112,7 @@ class MoveManager {
|
|||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
// Set default root if available
|
// Set default root if available
|
||||||
const settingsKey = `default_${currentPageType.slice(0, -1)}_root`;
|
const settingsKey = `default_${modelConfig.singularName}_root`;
|
||||||
const defaultRoot = state.global.settings[settingsKey];
|
const defaultRoot = state.global.settings[settingsKey];
|
||||||
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
if (defaultRoot && rootsData.roots.includes(defaultRoot)) {
|
||||||
modelRootSelect.value = defaultRoot;
|
modelRootSelect.value = defaultRoot;
|
||||||
@@ -228,13 +227,12 @@ class MoveManager {
|
|||||||
if (modelRoot) {
|
if (modelRoot) {
|
||||||
if (this.useDefaultPath) {
|
if (this.useDefaultPath) {
|
||||||
// Show actual template path
|
// Show actual template path
|
||||||
try {
|
const singularType = config.singularName || apiClient.modelType.replace(/s$/, '');
|
||||||
const singularType = apiClient.modelType.replace(/s$/, '');
|
const templates = state.global.settings.download_path_templates;
|
||||||
const templates = state.global.settings.download_path_templates;
|
const template = templates[singularType];
|
||||||
const template = templates[singularType];
|
if (template) {
|
||||||
fullPath += `/${template}`;
|
fullPath += `/${template}`;
|
||||||
} catch (error) {
|
} else {
|
||||||
console.error('Failed to fetch template:', error);
|
|
||||||
fullPath += '/' + translate('modals.download.autoOrganizedPath');
|
fullPath += '/' + translate('modals.download.autoOrganizedPath');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -298,7 +298,7 @@ export class SearchManager {
|
|||||||
pageState.searchOptions.loraName = options.loraName || false;
|
pageState.searchOptions.loraName = options.loraName || false;
|
||||||
pageState.searchOptions.loraModel = options.loraModel || false;
|
pageState.searchOptions.loraModel = options.loraModel || false;
|
||||||
pageState.searchOptions.prompt = options.prompt || false;
|
pageState.searchOptions.prompt = options.prompt || false;
|
||||||
} else if (this.currentPage === 'loras' || this.currentPage === 'checkpoints' || this.currentPage === 'embeddings') {
|
} else if (this.currentPage === 'loras' || this.currentPage === 'checkpoints' || this.currentPage === 'embeddings' || this.currentPage === 'other') {
|
||||||
// Update only the relevant fields in searchOptions instead of replacing the whole object
|
// Update only the relevant fields in searchOptions instead of replacing the whole object
|
||||||
pageState.searchOptions.filename = options.filename || false;
|
pageState.searchOptions.filename = options.filename || false;
|
||||||
pageState.searchOptions.modelname = options.modelname || false;
|
pageState.searchOptions.modelname = options.modelname || false;
|
||||||
@@ -311,7 +311,7 @@ export class SearchManager {
|
|||||||
// Call the appropriate manager's load method based on page type
|
// Call the appropriate manager's load method based on page type
|
||||||
if (this.currentPage === 'recipes' && window.recipeManager) {
|
if (this.currentPage === 'recipes' && window.recipeManager) {
|
||||||
window.recipeManager.loadRecipes(true);
|
window.recipeManager.loadRecipes(true);
|
||||||
} else if (this.currentPage === 'loras' || this.currentPage === 'embeddings' || this.currentPage === 'checkpoints') {
|
} else if (this.currentPage === 'loras' || this.currentPage === 'embeddings' || this.currentPage === 'checkpoints' || this.currentPage === 'other') {
|
||||||
// For models page, reset the page and reload
|
// For models page, reset the page and reload
|
||||||
getModelApiClient().loadMoreWithVirtualScroll(true, false);
|
getModelApiClient().loadMoreWithVirtualScroll(true, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3360,6 +3360,9 @@ export class SettingsManager {
|
|||||||
} else if (this.currentPage === 'embeddings') {
|
} else if (this.currentPage === 'embeddings') {
|
||||||
// Reload the embeddings without updating folders
|
// Reload the embeddings without updating folders
|
||||||
await resetAndReload(false);
|
await resetAndReload(false);
|
||||||
|
} else if (this.currentPage === 'other') {
|
||||||
|
// Reload the other models without updating folders
|
||||||
|
await resetAndReload(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { appCore } from './core.js';
|
||||||
|
import { confirmDelete, closeDeleteModal, confirmExclude, closeExcludeModal } from './utils/modalUtils.js';
|
||||||
|
import { createPageControls } from './components/controls/index.js';
|
||||||
|
import { ModelDuplicatesManager } from './components/ModelDuplicatesManager.js';
|
||||||
|
import { MODEL_TYPES } from './api/apiConfig.js';
|
||||||
|
import { initActiveFiltersSync } from './utils/activeFiltersSync.js';
|
||||||
|
|
||||||
|
// Initialize the Other Models page
|
||||||
|
class OtherPageManager {
|
||||||
|
constructor() {
|
||||||
|
// Initialize page controls
|
||||||
|
this.pageControls = createPageControls(MODEL_TYPES.OTHER);
|
||||||
|
|
||||||
|
// Initialize the ModelDuplicatesManager
|
||||||
|
this.duplicatesManager = new ModelDuplicatesManager(this, MODEL_TYPES.OTHER);
|
||||||
|
|
||||||
|
// Expose only necessary functions to global scope
|
||||||
|
this._exposeRequiredGlobalFunctions();
|
||||||
|
}
|
||||||
|
|
||||||
|
_exposeRequiredGlobalFunctions() {
|
||||||
|
// Minimal set of functions that need to remain global
|
||||||
|
window.confirmDelete = confirmDelete;
|
||||||
|
window.closeDeleteModal = closeDeleteModal;
|
||||||
|
window.confirmExclude = confirmExclude;
|
||||||
|
window.closeExcludeModal = closeExcludeModal;
|
||||||
|
|
||||||
|
// Expose duplicates manager
|
||||||
|
window.modelDuplicatesManager = this.duplicatesManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
// Initialize common page features (including context menus)
|
||||||
|
appCore.initializePageFeatures();
|
||||||
|
|
||||||
|
// Mirror active filters to the backend for the ComfyUI-side autocomplete
|
||||||
|
initActiveFiltersSync(MODEL_TYPES.OTHER);
|
||||||
|
|
||||||
|
console.log('Other Models Manager initialized');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeOtherPage() {
|
||||||
|
// Initialize core application
|
||||||
|
await appCore.initialize();
|
||||||
|
|
||||||
|
// Initialize other models page
|
||||||
|
const otherPage = new OtherPageManager();
|
||||||
|
await otherPage.initialize();
|
||||||
|
|
||||||
|
return otherPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize everything when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', initializeOtherPage);
|
||||||
|
|
||||||
|
export { OtherPageManager, initializeOtherPage };
|
||||||
@@ -79,6 +79,7 @@ export function createDefaultSettings() {
|
|||||||
const loraPreviewVersions = getMapFromStorage('loras_preview_versions');
|
const loraPreviewVersions = getMapFromStorage('loras_preview_versions');
|
||||||
const checkpointPreviewVersions = getMapFromStorage('checkpoints_preview_versions');
|
const checkpointPreviewVersions = getMapFromStorage('checkpoints_preview_versions');
|
||||||
const embeddingPreviewVersions = getMapFromStorage('embeddings_preview_versions');
|
const embeddingPreviewVersions = getMapFromStorage('embeddings_preview_versions');
|
||||||
|
const otherPreviewVersions = getMapFromStorage('other_preview_versions');
|
||||||
|
|
||||||
export const state = {
|
export const state = {
|
||||||
// Global state
|
// Global state
|
||||||
@@ -234,6 +235,44 @@ export const state = {
|
|||||||
search: '',
|
search: '',
|
||||||
},
|
},
|
||||||
activeViewSnapshot: null,
|
activeViewSnapshot: null,
|
||||||
|
},
|
||||||
|
|
||||||
|
[MODEL_TYPES.OTHER]: {
|
||||||
|
currentPage: 1,
|
||||||
|
isLoading: false,
|
||||||
|
hasMore: true,
|
||||||
|
sortBy: 'name',
|
||||||
|
activeFolder: getStorageItem(`${MODEL_TYPES.OTHER}_activeFolder`),
|
||||||
|
previewVersions: otherPreviewVersions,
|
||||||
|
searchManager: null,
|
||||||
|
searchOptions: {
|
||||||
|
filename: true,
|
||||||
|
modelname: true,
|
||||||
|
tags: false,
|
||||||
|
creator: false,
|
||||||
|
hash: false,
|
||||||
|
recursive: getStorageItem(`${MODEL_TYPES.OTHER}_recursiveSearch`, true),
|
||||||
|
},
|
||||||
|
filters: {
|
||||||
|
baseModel: [],
|
||||||
|
tags: {},
|
||||||
|
license: {},
|
||||||
|
modelTypes: [],
|
||||||
|
search: '',
|
||||||
|
tagLogic: 'any',
|
||||||
|
},
|
||||||
|
bulkMode: false,
|
||||||
|
selectedModels: new Set(),
|
||||||
|
metadataCache: new Map(),
|
||||||
|
showFavoritesOnly: false,
|
||||||
|
showUpdateAvailableOnly: false,
|
||||||
|
duplicatesMode: false,
|
||||||
|
viewMode: 'active',
|
||||||
|
excludedViewState: {
|
||||||
|
sortBy: 'name:asc',
|
||||||
|
search: '',
|
||||||
|
},
|
||||||
|
activeViewSnapshot: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function syncActiveFilters(pageType) {
|
|||||||
* Register the storage listener and push the current (restored) state once.
|
* Register the storage listener and push the current (restored) state once.
|
||||||
* The initial push covers server restarts, where the backend store is empty
|
* The initial push covers server restarts, where the backend store is empty
|
||||||
* until the manager page re-publishes its localStorage-restored filters.
|
* until the manager page re-publishes its localStorage-restored filters.
|
||||||
* @param {string} pageType - 'loras' | 'checkpoints' | 'embeddings'
|
* @param {string} pageType - 'loras' | 'checkpoints' | 'embeddings' | 'other'
|
||||||
*/
|
*/
|
||||||
export function initActiveFiltersSync(pageType) {
|
export function initActiveFiltersSync(pageType) {
|
||||||
setActiveFiltersListener((changedPageType) => syncActiveFilters(changedPageType));
|
setActiveFiltersListener((changedPageType) => syncActiveFilters(changedPageType));
|
||||||
|
|||||||
@@ -106,6 +106,12 @@ export const MODEL_SUBTYPE_DISPLAY_NAMES = {
|
|||||||
diffusion_model: "Diffusion Model",
|
diffusion_model: "Diffusion Model",
|
||||||
// Embedding sub-types
|
// Embedding sub-types
|
||||||
embedding: "Embedding",
|
embedding: "Embedding",
|
||||||
|
// Other model sub-types
|
||||||
|
vae: "VAE",
|
||||||
|
upscaler: "Upscaler",
|
||||||
|
text_encoder: "Text Encoder",
|
||||||
|
clip_vision: "CLIP Vision",
|
||||||
|
controlnet: "ControlNet",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Backward compatibility alias
|
// Backward compatibility alias
|
||||||
@@ -119,6 +125,11 @@ export const MODEL_SUBTYPE_ABBREVIATIONS = {
|
|||||||
checkpoint: "CKPT",
|
checkpoint: "CKPT",
|
||||||
diffusion_model: "DM",
|
diffusion_model: "DM",
|
||||||
embedding: "EMB",
|
embedding: "EMB",
|
||||||
|
vae: "VAE",
|
||||||
|
upscaler: "UPS",
|
||||||
|
text_encoder: "TE",
|
||||||
|
clip_vision: "CV",
|
||||||
|
controlnet: "CN",
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getSubTypeAbbreviation(subType) {
|
export function getSubTypeAbbreviation(subType) {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ async function getCardCreator(pageType) {
|
|||||||
|
|
||||||
// Function to get the appropriate data fetcher based on page type
|
// Function to get the appropriate data fetcher based on page type
|
||||||
async function getDataFetcher(pageType) {
|
async function getDataFetcher(pageType) {
|
||||||
if (pageType === 'loras' || pageType === 'embeddings' || pageType === 'checkpoints') {
|
if (pageType === 'loras' || pageType === 'embeddings' || pageType === 'checkpoints' || pageType === 'other') {
|
||||||
return (page = 1, pageSize = 100) => getModelApiClient().fetchModelsPage(page, pageSize);
|
return (page = 1, pageSize = 100) => getModelApiClient().fetchModelsPage(page, pageSize);
|
||||||
} else if (pageType === 'recipes') {
|
} else if (pageType === 'recipes') {
|
||||||
// Import the recipeApi module and use the fetchRecipesPage function
|
// Import the recipeApi module and use the fetchRecipesPage function
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const STORAGE_PREFIX = 'lora_manager_';
|
|||||||
|
|
||||||
// Matches keys that carry the manager page's active filter state
|
// Matches keys that carry the manager page's active filter state
|
||||||
// (e.g. 'loras_activeFolder', 'checkpoints_filters').
|
// (e.g. 'loras_activeFolder', 'checkpoints_filters').
|
||||||
const ACTIVE_FILTER_KEY_PATTERN = /^(loras|checkpoints|embeddings)_(activeFolder|recursiveSearch|filters)$/;
|
const ACTIVE_FILTER_KEY_PATTERN = /^(loras|checkpoints|embeddings|other)_(activeFolder|recursiveSearch|filters)$/;
|
||||||
|
|
||||||
let activeFiltersListener = null;
|
let activeFiltersListener = null;
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<option value="size:asc">{{ t('loras.controls.sort.sizeAsc') }}</option>
|
<option value="size:asc">{{ t('loras.controls.sort.sizeAsc') }}</option>
|
||||||
</optgroup>
|
</optgroup>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if page_id != 'embeddings' and page_id != 'recipes' %}
|
{% if page_id != 'embeddings' and page_id != 'recipes' and page_id != 'other' %}
|
||||||
<optgroup label="{{ t('loras.controls.sort.usage', default='Usage') }}">
|
<optgroup label="{{ t('loras.controls.sort.usage', default='Usage') }}">
|
||||||
<option value="usage:desc">{{ t('loras.controls.sort.usageDesc', default='Times used (high to low)') }}</option>
|
<option value="usage:desc">{{ t('loras.controls.sort.usageDesc', default='Times used (high to low)') }}</option>
|
||||||
<option value="usage:asc">{{ t('loras.controls.sort.usageAsc', default='Times used (low to high)') }}</option>
|
<option value="usage:asc">{{ t('loras.controls.sort.usageAsc', default='Times used (low to high)') }}</option>
|
||||||
@@ -80,12 +80,14 @@
|
|||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<button data-action="fetch" title="{{ t('loras.controls.fetch.title') }}"><i class="fas fa-download"></i> <span><span>{{ t('loras.controls.fetch.action') }}</span> <kbd class="shortcut-key">F</kbd></span></button>
|
<button data-action="fetch" title="{{ t('loras.controls.fetch.title') }}"><i class="fas fa-download"></i> <span><span>{{ t('loras.controls.fetch.action') }}</span> <kbd class="shortcut-key">F</kbd></span></button>
|
||||||
</div>
|
</div>
|
||||||
|
{% if page_id != 'other' %}
|
||||||
<div class="control-group">
|
<div class="control-group">
|
||||||
<button data-action="download" title="{{ t('loras.controls.download.title') }}">
|
<button data-action="download" title="{{ t('loras.controls.download.title') }}">
|
||||||
<i class="fas fa-cloud-download-alt"></i> <span><span>{{ t('loras.controls.download.action') }}</span> <kbd class="shortcut-key">D</kbd></span>
|
<i class="fas fa-cloud-download-alt"></i> <span><span>{{ t('loras.controls.download.action') }}</span> <kbd class="shortcut-key">D</kbd></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
{% if page_id == 'recipes' %}
|
{% if page_id == 'recipes' %}
|
||||||
<div title="{{ t('recipes.controls.import.title') }}" class="control-group">
|
<div title="{{ t('recipes.controls.import.title') }}" class="control-group">
|
||||||
<button onclick="importManager.showImportModal()"><i class="fas fa-file-import"></i> {{ t('recipes.controls.import.action') }}</button>
|
<button onclick="importManager.showImportModal()"><i class="fas fa-file-import"></i> {{ t('recipes.controls.import.action') }}</button>
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
{% set current_page = 'checkpoints' %}
|
{% set current_page = 'checkpoints' %}
|
||||||
{% elif current_path.startswith('/embeddings') %}
|
{% elif current_path.startswith('/embeddings') %}
|
||||||
{% set current_page = 'embeddings' %}
|
{% set current_page = 'embeddings' %}
|
||||||
|
{% elif current_path.startswith('/other') %}
|
||||||
|
{% set current_page = 'other' %}
|
||||||
{% elif current_path.startswith('/statistics') %}
|
{% elif current_path.startswith('/statistics') %}
|
||||||
{% set current_page = 'statistics' %}
|
{% set current_page = 'statistics' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -36,6 +38,10 @@
|
|||||||
id="embeddingsNavItem">
|
id="embeddingsNavItem">
|
||||||
<i class="fas fa-code"></i> <span>{{ t('header.navigation.embeddings') }}</span>
|
<i class="fas fa-code"></i> <span>{{ t('header.navigation.embeddings') }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/other" class="nav-item{% if current_path.startswith('/other') %} active{% endif %}"
|
||||||
|
id="otherNavItem">
|
||||||
|
<i class="fas fa-cubes"></i> <span>{{ t('header.navigation.other') }}</span>
|
||||||
|
</a>
|
||||||
<a href="/statistics" class="nav-item{% if current_path.startswith('/statistics') %} active{% endif %}"
|
<a href="/statistics" class="nav-item{% if current_path.startswith('/statistics') %} active{% endif %}"
|
||||||
id="statisticsNavItem">
|
id="statisticsNavItem">
|
||||||
<i class="fas fa-chart-bar"></i> <span>{{ t('header.navigation.statistics') }}</span>
|
<i class="fas fa-chart-bar"></i> <span>{{ t('header.navigation.statistics') }}</span>
|
||||||
@@ -208,7 +214,7 @@
|
|||||||
<div class="search-option-tag active" data-option="tags">{{ t('header.search.filters.tags') }}</div>
|
<div class="search-option-tag active" data-option="tags">{{ t('header.search.filters.tags') }}</div>
|
||||||
<div class="search-option-tag" data-option="creator">{{ t('header.search.filters.creator') }}</div>
|
<div class="search-option-tag" data-option="creator">{{ t('header.search.filters.creator') }}</div>
|
||||||
<div class="search-option-tag" data-option="hash">{{ t('header.search.filters.hash') }}</div>
|
<div class="search-option-tag" data-option="hash">{{ t('header.search.filters.hash') }}</div>
|
||||||
{% elif request.path == '/embeddings' %}
|
{% elif request.path == '/embeddings' or request.path == '/other' %}
|
||||||
<div class="search-option-tag active" data-option="filename">{{ t('header.search.filters.filename') }}</div>
|
<div class="search-option-tag active" data-option="filename">{{ t('header.search.filters.filename') }}</div>
|
||||||
<div class="search-option-tag active" data-option="modelname">{{ t('header.search.filters.modelname') }}</div>
|
<div class="search-option-tag active" data-option="modelname">{{ t('header.search.filters.modelname') }}</div>
|
||||||
<div class="search-option-tag active" data-option="tags">{{ t('header.search.filters.tags') }}</div>
|
<div class="search-option-tag active" data-option="tags">{{ t('header.search.filters.tags') }}</div>
|
||||||
@@ -295,7 +301,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_page == 'loras' or current_page == 'checkpoints' %}
|
{% if current_page == 'loras' or current_page == 'checkpoints' or current_page == 'other' %}
|
||||||
<div class="filter-section">
|
<div class="filter-section">
|
||||||
<h4>{{ t('header.filter.modelTypes') }}</h4>
|
<h4>{{ t('header.filter.modelTypes') }}</h4>
|
||||||
<div class="filter-tags" id="modelTypeTags">
|
<div class="filter-tags" id="modelTypeTags">
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ t('other.title') }}{% endblock %}
|
||||||
|
{% block page_id %}other{% endblock %}
|
||||||
|
|
||||||
|
{% block init_title %}{{ t('initialization.other.title') }}{% endblock %}
|
||||||
|
{% block init_message %}{{ t('initialization.other.message') }}{% endblock %}
|
||||||
|
{% block init_check_url %}/api/other/list?page=1&page_size=1{% endblock %}
|
||||||
|
|
||||||
|
{% block additional_components %}
|
||||||
|
|
||||||
|
<div id="otherContextMenu" class="context-menu" style="display: none;">
|
||||||
|
<!-- Metadata -->
|
||||||
|
<div class="context-menu-item" data-action="refresh-metadata"><i class="fas fa-sync"></i> {{ t('loras.contextMenu.refreshMetadata') }}</div>
|
||||||
|
<div class="context-menu-item has-submenu" data-has-submenu="link-model">
|
||||||
|
<i class="fas fa-link"></i>
|
||||||
|
<span>{{ t('loras.contextMenu.linkModel') }}</span>
|
||||||
|
<i class="fas fa-chevron-right submenu-arrow"></i>
|
||||||
|
<div class="context-submenu">
|
||||||
|
<div class="context-menu-item" data-action="relink-civitai">
|
||||||
|
<i class="fas fa-external-link-alt"></i> <span>{{ t('loras.contextMenu.linkCivitai') }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="link-hf">
|
||||||
|
<i class="fas fa-robot"></i> <span>{{ t('loras.contextMenu.linkHuggingFace') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Workflow -->
|
||||||
|
<div class="context-menu-item" data-action="copyname"><i class="fas fa-copy"></i> {{ t('loras.contextMenu.copyFilename') }}</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Media / Preview -->
|
||||||
|
<div class="context-menu-item" data-action="preview"><i class="fas fa-folder-open"></i> {{ t('loras.contextMenu.openExamples') }}</div>
|
||||||
|
<div class="context-menu-item has-submenu" data-has-submenu="download-examples"><i class="fas fa-download"></i> {{ t('loras.contextMenu.downloadExamples') }} <i class="fas fa-chevron-right submenu-arrow"></i>
|
||||||
|
<div class="context-submenu">
|
||||||
|
<div class="context-menu-item" data-action="download-examples"><i class="fas fa-download"></i> {{ t('loras.contextMenu.downloadMissingExamples') }}</div>
|
||||||
|
<div class="context-menu-item" data-action="download-examples-force"><i class="fas fa-redo-alt"></i> {{ t('loras.contextMenu.reprocessExamples') }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" data-action="replace-preview"><i class="fas fa-image"></i> {{ t('loras.contextMenu.replacePreview') }}</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Attributes -->
|
||||||
|
<div class="context-menu-item" data-action="set-nsfw"><i class="fas fa-exclamation-triangle"></i> {{ t('loras.contextMenu.setContentRating') }}</div>
|
||||||
|
<div class="context-menu-separator menu-section-break"></div>
|
||||||
|
<!-- Organization -->
|
||||||
|
<div class="context-menu-item" data-action="move"><i class="fas fa-folder-open"></i> {{ t('loras.contextMenu.moveToFolder') }}</div>
|
||||||
|
<div class="context-menu-separator"></div>
|
||||||
|
<!-- Destructive -->
|
||||||
|
<div class="context-menu-item" data-action="exclude"><i class="fas fa-eye-slash"></i> {{ t('loras.contextMenu.excludeModel') }}</div>
|
||||||
|
<div class="context-menu-item delete-item" data-action="delete"><i class="fas fa-trash"></i> {{ t('loras.contextMenu.deleteModel') }}</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="sticky-topbar">
|
||||||
|
{% include 'components/controls.html' %}
|
||||||
|
{% include 'components/breadcrumb.html' %}
|
||||||
|
</div>
|
||||||
|
{% include 'components/duplicates_banner.html' %}
|
||||||
|
{% include 'components/folder_sidebar.html' %}
|
||||||
|
|
||||||
|
<!-- Other model cards container -->
|
||||||
|
<div class="card-grid" id="modelGrid">
|
||||||
|
<!-- Cards will be dynamically inserted here -->
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block overlay %}
|
||||||
|
<div class="bulk-mode-overlay"></div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main_script %}
|
||||||
|
<script type="module" src="/loras_static/js/other.js?v={{ version }}"></script>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
// Import order matters: api modules are circularly dependent
|
||||||
|
// (modelApiFactory -> loraApi -> baseModelApi -> modelApiFactory/state).
|
||||||
|
// Loading the factory first lets baseModelApi fully evaluate before the
|
||||||
|
// client subclasses extend it.
|
||||||
|
import { createModelApiClient, getModelApiClient } from '../../../static/js/api/modelApiFactory.js';
|
||||||
|
import {
|
||||||
|
MODEL_TYPES,
|
||||||
|
MODEL_CONFIG,
|
||||||
|
getApiEndpoints,
|
||||||
|
getCompleteApiConfig,
|
||||||
|
isValidModelType,
|
||||||
|
} from '../../../static/js/api/apiConfig.js';
|
||||||
|
import { OtherApiClient } from '../../../static/js/api/otherApi.js';
|
||||||
|
|
||||||
|
describe('apiConfig - other model type', () => {
|
||||||
|
it('exposes OTHER model type', () => {
|
||||||
|
expect(MODEL_TYPES.OTHER).toBe('other');
|
||||||
|
expect(isValidModelType('other')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has a complete MODEL_CONFIG entry', () => {
|
||||||
|
const config = MODEL_CONFIG[MODEL_TYPES.OTHER];
|
||||||
|
|
||||||
|
expect(config).toBeDefined();
|
||||||
|
expect(config.singularName).toBe('other');
|
||||||
|
expect(config.supportsLetterFilter).toBe(false);
|
||||||
|
expect(config.supportsBulkOperations).toBe(true);
|
||||||
|
expect(config.supportsMove).toBe(true);
|
||||||
|
expect(config.templateName).toBe('other.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('generates /api/lm/other/* endpoints', () => {
|
||||||
|
const endpoints = getApiEndpoints('other');
|
||||||
|
|
||||||
|
expect(endpoints.list).toBe('/api/lm/other/list');
|
||||||
|
expect(endpoints.delete).toBe('/api/lm/other/delete');
|
||||||
|
expect(endpoints.exclude).toBe('/api/lm/other/exclude');
|
||||||
|
expect(endpoints.unexclude).toBe('/api/lm/other/unexclude');
|
||||||
|
expect(endpoints.rename).toBe('/api/lm/other/rename');
|
||||||
|
expect(endpoints.save).toBe('/api/lm/other/save-metadata');
|
||||||
|
expect(endpoints.bulkDelete).toBe('/api/lm/other/bulk-delete');
|
||||||
|
expect(endpoints.moveModel).toBe('/api/lm/other/move_model');
|
||||||
|
expect(endpoints.moveBulk).toBe('/api/lm/other/move_models_bulk');
|
||||||
|
expect(endpoints.fetchCivitai).toBe('/api/lm/other/fetch-civitai');
|
||||||
|
expect(endpoints.fetchAllCivitai).toBe('/api/lm/other/fetch-all-civitai');
|
||||||
|
expect(endpoints.scan).toBe('/api/lm/other/scan');
|
||||||
|
expect(endpoints.topTags).toBe('/api/lm/other/top-tags');
|
||||||
|
expect(endpoints.baseModels).toBe('/api/lm/other/base-models');
|
||||||
|
expect(endpoints.roots).toBe('/api/lm/other/roots');
|
||||||
|
expect(endpoints.folders).toBe('/api/lm/other/folders');
|
||||||
|
expect(endpoints.duplicates).toBe('/api/lm/other/find-duplicates');
|
||||||
|
expect(endpoints.replacePreview).toBe('/api/lm/other/replace-preview');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('merges other-specific endpoints into the complete config', () => {
|
||||||
|
const config = getCompleteApiConfig('other');
|
||||||
|
|
||||||
|
expect(config.modelType).toBe('other');
|
||||||
|
expect(config.config).toBe(MODEL_CONFIG.other);
|
||||||
|
expect(config.endpoints.specific.metadata).toBe('/api/lm/other/metadata');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('modelApiFactory - other model type', () => {
|
||||||
|
it('creates an OtherApiClient for the other model type', () => {
|
||||||
|
const client = createModelApiClient(MODEL_TYPES.OTHER);
|
||||||
|
|
||||||
|
expect(client).toBeInstanceOf(OtherApiClient);
|
||||||
|
expect(client.modelType).toBe('other');
|
||||||
|
expect(client.apiConfig.endpoints.list).toBe('/api/lm/other/list');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a cached singleton from getModelApiClient', () => {
|
||||||
|
const first = getModelApiClient(MODEL_TYPES.OTHER);
|
||||||
|
const second = getModelApiClient(MODEL_TYPES.OTHER);
|
||||||
|
|
||||||
|
expect(first).toBeInstanceOf(OtherApiClient);
|
||||||
|
expect(second).toBe(first);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still rejects unsupported model types', () => {
|
||||||
|
expect(() => createModelApiClient('bogus')).toThrow('Unsupported model type: bogus');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
|
||||||
|
const {
|
||||||
|
MODEL_CARD_MODULE,
|
||||||
|
STATE_MODULE,
|
||||||
|
UI_HELPERS_MODULE,
|
||||||
|
I18N_MODULE,
|
||||||
|
API_CONFIG_MODULE,
|
||||||
|
API_FACTORY_MODULE,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
|
MODEL_CARD_MODULE: new URL('../../../static/js/components/shared/ModelCard.js', import.meta.url).pathname,
|
||||||
|
STATE_MODULE: new URL('../../../static/js/state/index.js', import.meta.url).pathname,
|
||||||
|
UI_HELPERS_MODULE: new URL('../../../static/js/utils/uiHelpers.js', import.meta.url).pathname,
|
||||||
|
I18N_MODULE: new URL('../../../static/js/utils/i18nHelpers.js', import.meta.url).pathname,
|
||||||
|
API_CONFIG_MODULE: new URL('../../../static/js/api/apiConfig.js', import.meta.url).pathname,
|
||||||
|
API_FACTORY_MODULE: new URL('../../../static/js/api/modelApiFactory.js', import.meta.url).pathname,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(STATE_MODULE, () => ({
|
||||||
|
state: {
|
||||||
|
settings: {
|
||||||
|
blur_mature_content: false,
|
||||||
|
model_name_display: 'model_name',
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
settings: {
|
||||||
|
model_name_display: 'model_name',
|
||||||
|
group_by_model: false,
|
||||||
|
display_density: 'default',
|
||||||
|
model_card_footer_action: 'example_images',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pages: {
|
||||||
|
other: {
|
||||||
|
previewVersions: new Map(),
|
||||||
|
sortBy: 'name',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
bulkMode: false,
|
||||||
|
selectedModels: new Set(),
|
||||||
|
selectedLoras: new Set(),
|
||||||
|
},
|
||||||
|
getCurrentPageState: vi.fn(() => ({
|
||||||
|
sortBy: 'name',
|
||||||
|
previewVersions: new Map(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(UI_HELPERS_MODULE, () => ({
|
||||||
|
showToast: vi.fn(),
|
||||||
|
openCivitai: vi.fn(),
|
||||||
|
openHuggingFace: vi.fn(),
|
||||||
|
copyToClipboard: vi.fn(),
|
||||||
|
copyLoraSyntax: vi.fn(),
|
||||||
|
sendLoraToWorkflow: vi.fn(),
|
||||||
|
sendEmbeddingToWorkflow: vi.fn(),
|
||||||
|
openExampleImagesFolder: vi.fn(),
|
||||||
|
buildLoraSyntax: vi.fn(),
|
||||||
|
sendModelPathToWorkflow: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(I18N_MODULE, () => ({
|
||||||
|
translate: vi.fn((key, params, fallback) => (typeof fallback === 'string' ? fallback : key)),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(API_CONFIG_MODULE, () => ({
|
||||||
|
MODEL_TYPES: { LORA: 'loras', CHECKPOINT: 'checkpoints', EMBEDDING: 'embeddings', OTHER: 'other' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock(API_FACTORY_MODULE, () => ({
|
||||||
|
getModelApiClient: vi.fn(() => ({})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function createOtherModel(overrides = {}) {
|
||||||
|
return {
|
||||||
|
sha256: 'abc123',
|
||||||
|
file_path: '/models/vae/test_vae.safetensors',
|
||||||
|
model_name: 'Test VAE',
|
||||||
|
file_name: 'test_vae',
|
||||||
|
folder: 'vae',
|
||||||
|
modified: 1234567890,
|
||||||
|
file_size: 1024,
|
||||||
|
notes: '',
|
||||||
|
base_model: '',
|
||||||
|
favorite: false,
|
||||||
|
exclude: false,
|
||||||
|
hf_url: '',
|
||||||
|
update_available: false,
|
||||||
|
skip_metadata_refresh: false,
|
||||||
|
preview_url: '',
|
||||||
|
preview_nsfw_level: 0,
|
||||||
|
tags: [],
|
||||||
|
civitai: {},
|
||||||
|
sub_type: 'vae',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ModelCard sub-type badges for other model types', () => {
|
||||||
|
let createModelCard;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
({ createModelCard } = await import(MODEL_CARD_MODULE));
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['vae', 'VAE', 'VAE'],
|
||||||
|
['upscaler', 'UPS', 'Upscaler'],
|
||||||
|
['text_encoder', 'TE', 'Text Encoder'],
|
||||||
|
['clip_vision', 'CV', 'CLIP Vision'],
|
||||||
|
['controlnet', 'CN', 'ControlNet'],
|
||||||
|
])('renders the %s badge abbreviation and tooltip', (subType, abbreviation, displayName) => {
|
||||||
|
const card = createModelCard(createOtherModel({ sub_type: subType }), 'other');
|
||||||
|
|
||||||
|
const badge = card.querySelector('.model-sub-type');
|
||||||
|
expect(badge).not.toBeNull();
|
||||||
|
expect(badge.textContent).toBe(abbreviation);
|
||||||
|
|
||||||
|
const label = card.querySelector('.base-model-label');
|
||||||
|
expect(label.getAttribute('title')).toContain(displayName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores sub_type on the card dataset', () => {
|
||||||
|
const card = createModelCard(createOtherModel({ sub_type: 'text_encoder' }), 'other');
|
||||||
|
|
||||||
|
expect(card.dataset.sub_type).toBe('text_encoder');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest';
|
||||||
|
|
||||||
|
const {
|
||||||
|
getModelApiClientMock,
|
||||||
|
resetAndReloadMock,
|
||||||
|
showToastMock,
|
||||||
|
sidebarManagerMock,
|
||||||
|
moveManagerMock,
|
||||||
|
showDeleteModalMock,
|
||||||
|
showExcludeModalMock,
|
||||||
|
} = vi.hoisted(() => ({
|
||||||
|
getModelApiClientMock: vi.fn(),
|
||||||
|
resetAndReloadMock: vi.fn(async () => {}),
|
||||||
|
showToastMock: vi.fn(),
|
||||||
|
sidebarManagerMock: {
|
||||||
|
setHostPageControls: vi.fn(),
|
||||||
|
initialize: vi.fn(async function () {
|
||||||
|
sidebarManagerMock.isInitialized = true;
|
||||||
|
}),
|
||||||
|
refresh: vi.fn(async () => {}),
|
||||||
|
cleanup: vi.fn(),
|
||||||
|
isInitialized: false,
|
||||||
|
},
|
||||||
|
moveManagerMock: {
|
||||||
|
showMoveModal: vi.fn(),
|
||||||
|
},
|
||||||
|
showDeleteModalMock: vi.fn(),
|
||||||
|
showExcludeModalMock: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/api/modelApiFactory.js', () => ({
|
||||||
|
getModelApiClient: getModelApiClientMock,
|
||||||
|
resetAndReload: resetAndReloadMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
|
||||||
|
showToast: showToastMock,
|
||||||
|
openCivitaiByMetadata: vi.fn(),
|
||||||
|
isTypingContext: () => false,
|
||||||
|
getNSFWLevelName: vi.fn(() => 'Unknown'),
|
||||||
|
openExampleImagesFolder: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/managers/DownloadManager.js', () => ({
|
||||||
|
downloadManager: { showDownloadModal: vi.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/components/SidebarManager.js', () => ({
|
||||||
|
sidebarManager: sidebarManagerMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/managers/MoveManager.js', () => ({
|
||||||
|
moveManager: moveManagerMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/utils/modalUtils.js', () => ({
|
||||||
|
showDeleteModal: showDeleteModalMock,
|
||||||
|
showExcludeModal: showExcludeModalMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/components/alphabet/index.js', () => ({
|
||||||
|
createAlphabetBar: vi.fn(() => ({ destroy: vi.fn() })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/utils/updateCheckHelpers.js', () => ({
|
||||||
|
performModelUpdateCheck: vi.fn(async () => ({ status: 'success', displayName: 'Model', records: [] })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createPageControls } from '../../../static/js/components/controls/index.js';
|
||||||
|
import { OtherControls } from '../../../static/js/components/controls/OtherControls.js';
|
||||||
|
import { createPageContextMenu } from '../../../static/js/components/ContextMenu/index.js';
|
||||||
|
import { OtherContextMenu } from '../../../static/js/components/ContextMenu/OtherContextMenu.js';
|
||||||
|
|
||||||
|
describe('createPageControls', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
|
document.body.innerHTML = '';
|
||||||
|
document.body.dataset.page = 'other';
|
||||||
|
sidebarManagerMock.isInitialized = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete window.pageControls;
|
||||||
|
delete window.bulkManager;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates OtherControls for the other page type', () => {
|
||||||
|
const controls = createPageControls('other');
|
||||||
|
|
||||||
|
expect(controls).toBeInstanceOf(OtherControls);
|
||||||
|
expect(controls.pageType).toBe('other');
|
||||||
|
// OtherControls registers its API with the base class
|
||||||
|
expect(typeof controls.api.loadMoreModels).toBe('function');
|
||||||
|
expect(typeof controls.api.refreshModels).toBe('function');
|
||||||
|
expect(typeof controls.api.fetchFromCivitai).toBe('function');
|
||||||
|
expect(typeof controls.api.toggleBulkMode).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for an unknown page type', () => {
|
||||||
|
expect(createPageControls('not-a-page')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createPageContextMenu', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
document.body.innerHTML = '<div id="otherContextMenu" class="context-menu" style="display: none;"></div>';
|
||||||
|
});
|
||||||
|
|
||||||
|
function createMenuWithCard() {
|
||||||
|
const menu = createPageContextMenu('other');
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'model-card';
|
||||||
|
card.dataset.filepath = '/models/vae/test.safetensors';
|
||||||
|
document.body.appendChild(card);
|
||||||
|
menu.currentCard = card;
|
||||||
|
return { menu, card };
|
||||||
|
}
|
||||||
|
|
||||||
|
it('creates OtherContextMenu for the other page type', () => {
|
||||||
|
const menu = createPageContextMenu('other');
|
||||||
|
|
||||||
|
expect(menu).toBeInstanceOf(OtherContextMenu);
|
||||||
|
expect(menu.modelType).toBe('other');
|
||||||
|
expect(menu.menu).toBe(document.getElementById('otherContextMenu'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns null for an unknown page type', () => {
|
||||||
|
expect(createPageContextMenu('not-a-page')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('delegates refresh-metadata to the model API client', () => {
|
||||||
|
const refreshSingleModelMetadata = vi.fn();
|
||||||
|
getModelApiClientMock.mockReturnValue({ refreshSingleModelMetadata });
|
||||||
|
const { menu } = createMenuWithCard();
|
||||||
|
|
||||||
|
menu.handleMenuAction('refresh-metadata');
|
||||||
|
|
||||||
|
expect(refreshSingleModelMetadata).toHaveBeenCalledWith('/models/vae/test.safetensors');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens the move modal for the move action', () => {
|
||||||
|
const { menu } = createMenuWithCard();
|
||||||
|
|
||||||
|
menu.handleMenuAction('move');
|
||||||
|
|
||||||
|
expect(moveManagerMock.showMoveModal).toHaveBeenCalledWith('/models/vae/test.safetensors');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows the exclude modal for the exclude action', () => {
|
||||||
|
const { menu } = createMenuWithCard();
|
||||||
|
|
||||||
|
menu.handleMenuAction('exclude');
|
||||||
|
|
||||||
|
expect(showExcludeModalMock).toHaveBeenCalledWith('/models/vae/test.safetensors');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -120,7 +120,7 @@ import { initializeEventManagement } from '../../../static/js/utils/eventManagem
|
|||||||
import { initializeInfiniteScroll } from '../../../static/js/utils/infiniteScroll.js';
|
import { initializeInfiniteScroll } from '../../../static/js/utils/infiniteScroll.js';
|
||||||
import { createPageContextMenu, createGlobalContextMenu } from '../../../static/js/components/ContextMenu/index.js';
|
import { createPageContextMenu, createGlobalContextMenu } from '../../../static/js/components/ContextMenu/index.js';
|
||||||
|
|
||||||
const SUPPORTED_PAGES = ['loras', 'recipes', 'checkpoints', 'embeddings'];
|
const SUPPORTED_PAGES = ['loras', 'recipes', 'checkpoints', 'embeddings', 'other'];
|
||||||
|
|
||||||
describe('AppCore page orchestration', () => {
|
describe('AppCore page orchestration', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||||
|
import { renderOtherPage } from '../utils/pageFixtures.js';
|
||||||
|
|
||||||
|
const initializeAppMock = vi.fn();
|
||||||
|
const initializePageFeaturesMock = vi.fn();
|
||||||
|
const createPageControlsMock = vi.fn();
|
||||||
|
const confirmDeleteMock = vi.fn();
|
||||||
|
const closeDeleteModalMock = vi.fn();
|
||||||
|
const confirmExcludeMock = vi.fn();
|
||||||
|
const closeExcludeModalMock = vi.fn();
|
||||||
|
const duplicatesManagerMock = vi.fn();
|
||||||
|
const initActiveFiltersSyncMock = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/core.js', () => ({
|
||||||
|
appCore: {
|
||||||
|
initialize: initializeAppMock,
|
||||||
|
initializePageFeatures: initializePageFeaturesMock,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/components/controls/index.js', () => ({
|
||||||
|
createPageControls: createPageControlsMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/utils/modalUtils.js', () => ({
|
||||||
|
confirmDelete: confirmDeleteMock,
|
||||||
|
closeDeleteModal: closeDeleteModalMock,
|
||||||
|
confirmExclude: confirmExcludeMock,
|
||||||
|
closeExcludeModal: closeExcludeModalMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/api/apiConfig.js', () => ({
|
||||||
|
MODEL_TYPES: {
|
||||||
|
OTHER: 'other',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/components/ModelDuplicatesManager.js', () => ({
|
||||||
|
ModelDuplicatesManager: duplicatesManagerMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../../static/js/utils/activeFiltersSync.js', () => ({
|
||||||
|
initActiveFiltersSync: initActiveFiltersSyncMock,
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('OtherPageManager', () => {
|
||||||
|
let OtherPageManager;
|
||||||
|
let initializeOtherPage;
|
||||||
|
let duplicatesManagerInstance;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.resetModules();
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
duplicatesManagerInstance = {
|
||||||
|
checkDuplicatesCount: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
duplicatesManagerMock.mockReturnValue(duplicatesManagerInstance);
|
||||||
|
createPageControlsMock.mockReturnValue({ destroy: vi.fn() });
|
||||||
|
initializeAppMock.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
renderOtherPage();
|
||||||
|
|
||||||
|
({ OtherPageManager, initializeOtherPage } = await import('../../../static/js/other.js'));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete window.confirmDelete;
|
||||||
|
delete window.closeDeleteModal;
|
||||||
|
delete window.confirmExclude;
|
||||||
|
delete window.closeExcludeModal;
|
||||||
|
delete window.modelDuplicatesManager;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wires page controls and exposes modal helpers during construction', () => {
|
||||||
|
const manager = new OtherPageManager();
|
||||||
|
|
||||||
|
expect(createPageControlsMock).toHaveBeenCalledWith('other');
|
||||||
|
expect(duplicatesManagerMock).toHaveBeenCalledWith(manager, 'other');
|
||||||
|
|
||||||
|
expect(window.confirmDelete).toBe(confirmDeleteMock);
|
||||||
|
expect(window.closeDeleteModal).toBe(closeDeleteModalMock);
|
||||||
|
expect(window.confirmExclude).toBe(confirmExcludeMock);
|
||||||
|
expect(window.closeExcludeModal).toBe(closeExcludeModalMock);
|
||||||
|
expect(window.modelDuplicatesManager).toBe(duplicatesManagerInstance);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes shared page features and syncs active filters', async () => {
|
||||||
|
const manager = new OtherPageManager();
|
||||||
|
|
||||||
|
await manager.initialize();
|
||||||
|
|
||||||
|
expect(initializePageFeaturesMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(initActiveFiltersSyncMock).toHaveBeenCalledWith('other');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('boots the other models page through the initializer', async () => {
|
||||||
|
const manager = await initializeOtherPage();
|
||||||
|
|
||||||
|
expect(initializeAppMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(manager).toBeInstanceOf(OtherPageManager);
|
||||||
|
expect(window.modelDuplicatesManager).toBe(duplicatesManagerInstance);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -36,6 +36,18 @@ export function renderEmbeddingsPage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the Other Models page template with expected dataset attributes.
|
||||||
|
* @returns {Element}
|
||||||
|
*/
|
||||||
|
export function renderOtherPage() {
|
||||||
|
return renderTemplate('other.html', {
|
||||||
|
dataset: {
|
||||||
|
page: 'other',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the Recipes page template with expected dataset attributes.
|
* Renders the Recipes page template with expected dataset attributes.
|
||||||
* @returns {Element}
|
* @returns {Element}
|
||||||
|
|||||||
Reference in New Issue
Block a user