mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-09-21 03:01:27 -03:00
Compare commits
11 Commits
b309becdf9
...
93fc28b499
| Author | SHA1 | Date | |
|---|---|---|---|
| 93fc28b499 | |||
| 7afed1a14b | |||
| e6f5142e48 | |||
| 87f05fb66c | |||
| cf64e5baa8 | |||
| 634ea7f299 | |||
| 6ba64ebb3c | |||
| 03569c62df | |||
| a61840b366 | |||
| 726fc178f1 | |||
| 8260bd022d |
+61
-6
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"bulk": {
|
||||
"title": "Massenoperationen",
|
||||
"content": "Wechseln Sie in den Massenmodus, indem Sie auf diese Schaltfläche klicken oder <span class=\"onboarding-shortcut\">B</span> drücken. Wählen Sie mehrere Modelle aus und führen Sie Stapeloperationen durch. Mit <span class=\"onboarding-shortcut\">Strg+A</span> können Sie alle sichtbaren Modelle auswählen."
|
||||
"content": "Wechseln Sie in den Massenmodus, indem Sie auf diese Schaltfläche klicken oder <span class=\"onboarding-shortcut\">B</span> drücken, um mehrere Modelle auszuwählen und Stapeloperationen durchzuführen.<br>• <span class=\"onboarding-shortcut\">Ctrl/Cmd+A</span> wählt alle sichtbaren Modelle aus, <span class=\"onboarding-shortcut\">Shift+Click</span> wählt einen Bereich aus.<br>• <span class=\"onboarding-shortcut\">Esc</span> oder ein Klick auf einen leeren Bereich verlässt den Massenmodus."
|
||||
},
|
||||
"searchOptions": {
|
||||
"title": "Suchoptionen",
|
||||
@@ -116,7 +116,19 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"title": "Kontextmenü",
|
||||
"content": "<strong>Rechtsklick</strong> auf eine Modellkarte öffnet ein Kontextmenü mit weiteren Aktionen."
|
||||
"content": "<strong>Rechtsklick</strong> auf eine beliebige Modellkarte öffnet ein Kontextmenü mit Kartenaktionen wie Verschieben, Löschen oder Bearbeiten von Metadaten."
|
||||
},
|
||||
"marqueeSelect": {
|
||||
"title": "Durch Ziehen auswählen",
|
||||
"content": "Halten Sie die <strong>linke Maustaste</strong> auf einem leeren Bereich des Rasters gedrückt und ziehen Sie, um einen Auswahlrahmen aufzuziehen, der mehrere Karten gleichzeitig auswählt."
|
||||
},
|
||||
"dragToSidebar": {
|
||||
"title": "Organisieren durch Ziehen",
|
||||
"content": "Ziehen Sie eine Modellkarte auf einen Ordner in der Seitenleiste, um die Datei dorthin zu verschieben. Dies funktioniert auch mit mehreren ausgewählten Karten im Massenmodus."
|
||||
},
|
||||
"contextMenus": {
|
||||
"title": "Weitere Kontextmenüs",
|
||||
"content": "<strong>Rechtsklick auf eine ausgewählte Karte</strong> im Massenmodus öffnet die Massenaktionen. <strong>Rechtsklick auf einen leeren Bereich</strong> der Seite öffnet globale Aktionen wie das Prüfen auf Updates und das Verwalten ausgeschlossener Modelle."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1629,7 +1641,7 @@
|
||||
"addPlaceholder": "Tippen zum Hinzufügen oder klicken Sie auf Vorschläge unten",
|
||||
"editWord": "Trigger Word bearbeiten",
|
||||
"editPlaceholder": "Trigger Word bearbeiten",
|
||||
"copyWord": "Trigger Word kopieren",
|
||||
"copyOrEditWord": "Klicken zum Kopieren, Doppelklick zum Bearbeiten",
|
||||
"deleteWord": "Trigger Word löschen",
|
||||
"suggestions": {
|
||||
"noSuggestions": "Keine Vorschläge verfügbar",
|
||||
@@ -1954,10 +1966,52 @@
|
||||
"tabs": {
|
||||
"gettingStarted": "Erste Schritte",
|
||||
"updateVlogs": "Update-Vlogs",
|
||||
"documentation": "Dokumentation"
|
||||
"documentation": "Dokumentation",
|
||||
"shortcuts": "Tastenkürzel"
|
||||
},
|
||||
"gettingStarted": {
|
||||
"title": "Erste Schritte mit LoRA Manager"
|
||||
"title": "Erste Schritte mit LoRA Manager",
|
||||
"replayTutorial": "Tutorial erneut abspielen"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Tastatur- & Mauskürzel",
|
||||
"groups": {
|
||||
"general": "Allgemein",
|
||||
"actions": "Aktionen",
|
||||
"selection": "Auswahl & Massenmodus",
|
||||
"navigation": "Navigation",
|
||||
"modelModal": "Modell- / Rezept-Dialog",
|
||||
"mediaViewer": "Medienanzeige / Beispielgalerie"
|
||||
},
|
||||
"keys": {
|
||||
"click": "Klick",
|
||||
"drag": "Ziehen",
|
||||
"rightClick": "Rechtsklick",
|
||||
"letter": "Buchstabe",
|
||||
"swipe": "Wischen"
|
||||
},
|
||||
"entries": {
|
||||
"focusSearch": "Suche fokussieren",
|
||||
"closeModal": "Dialog / Panel schließen",
|
||||
"openShortcuts": "Dieses Tastenkürzel-Panel öffnen",
|
||||
"refresh": "Modellliste aktualisieren",
|
||||
"fetchMetadata": "Metadaten von CivitAI abrufen (nur Modellseiten)",
|
||||
"downloadModel": "Ein Modell herunterladen (nur Modellseiten)",
|
||||
"toggleBulkMode": "Massenmodus umschalten",
|
||||
"selectAll": "Alle sichtbaren Modelle auswählen",
|
||||
"rangeSelect": "Bereich auswählen",
|
||||
"marqueeSelect": "Karten mit Auswahlrahmen auswählen (auf leerem Rasterbereich)",
|
||||
"exitBulkMode": "Massenmodus verlassen",
|
||||
"bulkActions": "Auf ausgewählter Karte: Menü für Massenaktionen",
|
||||
"globalActions": "Auf leerem Seitenbereich: Menü für globale Aktionen (Updates prüfen, ausgeschlossene Modelle verwalten)",
|
||||
"scrollPages": "Seiten scrollen",
|
||||
"jumpAlphabet": "Zur Alphabetleiste springen",
|
||||
"prevNext": "Vorheriges / nächstes Modell",
|
||||
"deleteEntry": "Löschen",
|
||||
"cycleMedia": "Medien durchblättern ([ / ] in der Beispielgalerie)",
|
||||
"swipeTouch": "Medien auf Touch-Geräten durchblättern",
|
||||
"closeViewer": "Medienanzeige schließen"
|
||||
}
|
||||
},
|
||||
"updateVlogs": {
|
||||
"title": "Neueste Updates",
|
||||
@@ -1974,7 +2028,8 @@
|
||||
"settings": "Einstellungen & Konfiguration",
|
||||
"extensions": "Erweiterungen",
|
||||
"newBadge": "NEU"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "NEU"
|
||||
},
|
||||
"update": {
|
||||
"title": "Nach Updates suchen",
|
||||
|
||||
+61
-6
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"bulk": {
|
||||
"title": "Bulk Operations",
|
||||
"content": "Enter bulk mode by clicking this button or pressing <span class=\"onboarding-shortcut\">B</span>. Select multiple models and perform batch operations. Use <span class=\"onboarding-shortcut\">Ctrl+A</span> to select all visible models."
|
||||
"content": "Enter bulk mode by clicking this button or pressing <span class=\"onboarding-shortcut\">B</span> to select multiple models and perform batch operations.<br>• <span class=\"onboarding-shortcut\">Ctrl/Cmd+A</span> select all visible models, <span class=\"onboarding-shortcut\">Shift+Click</span> select a range.<br>• <span class=\"onboarding-shortcut\">Esc</span> or clicking an empty area exits bulk mode."
|
||||
},
|
||||
"searchOptions": {
|
||||
"title": "Search Options",
|
||||
@@ -116,7 +116,19 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"title": "Context Menu",
|
||||
"content": "<strong>Right-click</strong> any model card for a context menu with additional actions."
|
||||
"content": "<strong>Right-click</strong> any model card for a context menu with card actions like moving, deleting, or editing metadata."
|
||||
},
|
||||
"marqueeSelect": {
|
||||
"title": "Drag to Select",
|
||||
"content": "Hold the <strong>left mouse button</strong> on an empty area of the grid and drag to draw a marquee that selects multiple cards at once."
|
||||
},
|
||||
"dragToSidebar": {
|
||||
"title": "Organize by Dragging",
|
||||
"content": "Drag a model card onto a folder in the sidebar to move the file there. This also works with multiple selected cards in bulk mode."
|
||||
},
|
||||
"contextMenus": {
|
||||
"title": "More Context Menus",
|
||||
"content": "In bulk mode, <strong>right-click a selected card</strong> for bulk actions. <strong>Right-click an empty area</strong> of the page for global actions like update checks and managing excluded models."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1629,7 +1641,7 @@
|
||||
"addPlaceholder": "Type to add or click suggestions below",
|
||||
"editWord": "Edit trigger word",
|
||||
"editPlaceholder": "Edit trigger word",
|
||||
"copyWord": "Copy trigger word",
|
||||
"copyOrEditWord": "Click to copy, double-click to edit",
|
||||
"deleteWord": "Delete trigger word",
|
||||
"suggestions": {
|
||||
"noSuggestions": "No suggestions available",
|
||||
@@ -1954,10 +1966,52 @@
|
||||
"tabs": {
|
||||
"gettingStarted": "Getting Started",
|
||||
"updateVlogs": "Update Vlogs",
|
||||
"documentation": "Documentation"
|
||||
"documentation": "Documentation",
|
||||
"shortcuts": "Shortcuts"
|
||||
},
|
||||
"gettingStarted": {
|
||||
"title": "Getting Started with LoRA Manager"
|
||||
"title": "Getting Started with LoRA Manager",
|
||||
"replayTutorial": "Replay Tutorial"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Keyboard & Mouse Shortcuts",
|
||||
"groups": {
|
||||
"general": "General",
|
||||
"actions": "Actions",
|
||||
"selection": "Selection & Bulk Mode",
|
||||
"navigation": "Navigation",
|
||||
"modelModal": "Model / Recipe Modal",
|
||||
"mediaViewer": "Media Viewer / Showcase"
|
||||
},
|
||||
"keys": {
|
||||
"click": "Click",
|
||||
"drag": "Drag",
|
||||
"rightClick": "Right-click",
|
||||
"letter": "Letter",
|
||||
"swipe": "Swipe"
|
||||
},
|
||||
"entries": {
|
||||
"focusSearch": "Focus search",
|
||||
"closeModal": "Close modal / panel",
|
||||
"openShortcuts": "Open this shortcuts panel",
|
||||
"refresh": "Refresh model list",
|
||||
"fetchMetadata": "Fetch metadata from CivitAI (model pages only)",
|
||||
"downloadModel": "Download a model (model pages only)",
|
||||
"toggleBulkMode": "Toggle bulk mode",
|
||||
"selectAll": "Select all visible models",
|
||||
"rangeSelect": "Range select",
|
||||
"marqueeSelect": "Marquee-select cards (on empty grid area)",
|
||||
"exitBulkMode": "Exit bulk mode",
|
||||
"bulkActions": "On selected card: bulk actions menu",
|
||||
"globalActions": "On empty page area: global actions menu (update check, manage excluded models)",
|
||||
"scrollPages": "Scroll pages",
|
||||
"jumpAlphabet": "Jump alphabet bar",
|
||||
"prevNext": "Previous / next model",
|
||||
"deleteEntry": "Delete",
|
||||
"cycleMedia": "Cycle media ([ / ] in showcase gallery)",
|
||||
"swipeTouch": "Cycle media on touch devices",
|
||||
"closeViewer": "Close viewer"
|
||||
}
|
||||
},
|
||||
"updateVlogs": {
|
||||
"title": "Latest Updates",
|
||||
@@ -1974,7 +2028,8 @@
|
||||
"settings": "Settings & Configuration",
|
||||
"extensions": "Extensions",
|
||||
"newBadge": "NEW"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "New"
|
||||
},
|
||||
"update": {
|
||||
"title": "Check for Updates",
|
||||
|
||||
+61
-6
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"bulk": {
|
||||
"title": "Operaciones por lotes",
|
||||
"content": "Entra en el modo por lotes haciendo clic en este botón o presionando <span class=\"onboarding-shortcut\">B</span>. Selecciona varios modelos y realiza operaciones por lotes. Usa <span class=\"onboarding-shortcut\">Ctrl+A</span> para seleccionar todos los modelos visibles."
|
||||
"content": "Entra en el modo por lotes haciendo clic en este botón o presionando <span class=\"onboarding-shortcut\">B</span> para seleccionar varios modelos y realizar operaciones por lotes.<br>• <span class=\"onboarding-shortcut\">Ctrl/Cmd+A</span> selecciona todos los modelos visibles, <span class=\"onboarding-shortcut\">Shift+Click</span> selecciona un rango.<br>• <span class=\"onboarding-shortcut\">Esc</span> o hacer clic en un área vacía sale del modo por lotes."
|
||||
},
|
||||
"searchOptions": {
|
||||
"title": "Opciones de búsqueda",
|
||||
@@ -116,7 +116,19 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"title": "Menú contextual",
|
||||
"content": "<strong>Clic derecho</strong> en cualquier tarjeta de modelo para ver un menú contextual con acciones adicionales."
|
||||
"content": "<strong>Clic derecho</strong> en cualquier tarjeta de modelo para ver un menú contextual con acciones de la tarjeta como mover, eliminar o editar metadatos."
|
||||
},
|
||||
"marqueeSelect": {
|
||||
"title": "Arrastrar para seleccionar",
|
||||
"content": "Mantén pulsado el <strong>botón izquierdo del ratón</strong> en un área vacía de la cuadrícula y arrastra para dibujar un rectángulo de selección que selecciona varias tarjetas a la vez."
|
||||
},
|
||||
"dragToSidebar": {
|
||||
"title": "Organizar arrastrando",
|
||||
"content": "Arrastra una tarjeta de modelo hasta una carpeta de la barra lateral para mover el archivo allí. Esto también funciona con varias tarjetas seleccionadas en el modo por lotes."
|
||||
},
|
||||
"contextMenus": {
|
||||
"title": "Más menús contextuales",
|
||||
"content": "En el modo por lotes, <strong>haz clic derecho en una tarjeta seleccionada</strong> para ver las acciones por lotes. <strong>Haz clic derecho en un área vacía</strong> de la página para ver acciones globales como comprobar actualizaciones y gestionar modelos excluidos."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1629,7 +1641,7 @@
|
||||
"addPlaceholder": "Escribe para añadir o haz clic en sugerencias de abajo",
|
||||
"editWord": "Editar palabra de activación",
|
||||
"editPlaceholder": "Editar palabra de activación",
|
||||
"copyWord": "Copiar palabra de activación",
|
||||
"copyOrEditWord": "Haz clic para copiar, doble clic para editar",
|
||||
"deleteWord": "Eliminar palabra de activación",
|
||||
"suggestions": {
|
||||
"noSuggestions": "No hay sugerencias disponibles",
|
||||
@@ -1954,10 +1966,52 @@
|
||||
"tabs": {
|
||||
"gettingStarted": "Comenzando",
|
||||
"updateVlogs": "Vlogs de actualización",
|
||||
"documentation": "Documentación"
|
||||
"documentation": "Documentación",
|
||||
"shortcuts": "Atajos"
|
||||
},
|
||||
"gettingStarted": {
|
||||
"title": "Comenzando con el gestor de LoRA"
|
||||
"title": "Comenzando con el gestor de LoRA",
|
||||
"replayTutorial": "Repetir tutorial"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Atajos de teclado y ratón",
|
||||
"groups": {
|
||||
"general": "General",
|
||||
"actions": "Acciones",
|
||||
"selection": "Selección y modo por lotes",
|
||||
"navigation": "Navegación",
|
||||
"modelModal": "Modal de modelo / receta",
|
||||
"mediaViewer": "Visor de medios / Ejemplos"
|
||||
},
|
||||
"keys": {
|
||||
"click": "Clic",
|
||||
"drag": "Arrastrar",
|
||||
"rightClick": "Clic derecho",
|
||||
"letter": "Letra",
|
||||
"swipe": "Deslizar"
|
||||
},
|
||||
"entries": {
|
||||
"focusSearch": "Enfocar la búsqueda",
|
||||
"closeModal": "Cerrar modal / panel",
|
||||
"openShortcuts": "Abrir este panel de atajos",
|
||||
"refresh": "Actualizar la lista de modelos",
|
||||
"fetchMetadata": "Obtener metadatos de CivitAI (solo páginas de modelos)",
|
||||
"downloadModel": "Descargar un modelo (solo páginas de modelos)",
|
||||
"toggleBulkMode": "Activar/desactivar el modo por lotes",
|
||||
"selectAll": "Seleccionar todos los modelos visibles",
|
||||
"rangeSelect": "Selección por rango",
|
||||
"marqueeSelect": "Seleccionar tarjetas con un rectángulo de selección (en un área vacía de la cuadrícula)",
|
||||
"exitBulkMode": "Salir del modo por lotes",
|
||||
"bulkActions": "En una tarjeta seleccionada: menú de acciones por lotes",
|
||||
"globalActions": "En un área vacía de la página: menú de acciones globales (comprobar actualizaciones, gestionar modelos excluidos)",
|
||||
"scrollPages": "Desplazarse por las páginas",
|
||||
"jumpAlphabet": "Saltar con la barra alfabética",
|
||||
"prevNext": "Modelo anterior / siguiente",
|
||||
"deleteEntry": "Eliminar",
|
||||
"cycleMedia": "Cambiar de medio ([ / ] en la galería de ejemplos)",
|
||||
"swipeTouch": "Cambiar de medio en dispositivos táctiles",
|
||||
"closeViewer": "Cerrar el visor"
|
||||
}
|
||||
},
|
||||
"updateVlogs": {
|
||||
"title": "Últimas actualizaciones",
|
||||
@@ -1974,7 +2028,8 @@
|
||||
"settings": "Configuración",
|
||||
"extensions": "Extensiones",
|
||||
"newBadge": "NUEVO"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "NUEVO"
|
||||
},
|
||||
"update": {
|
||||
"title": "Comprobar actualizaciones",
|
||||
|
||||
+61
-6
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"bulk": {
|
||||
"title": "Opérations groupées",
|
||||
"content": "Activez le mode groupé en cliquant sur ce bouton ou en appuyant sur <span class=\"onboarding-shortcut\">B</span>. Sélectionnez plusieurs modèles et effectuez des opérations groupées. Utilisez <span class=\"onboarding-shortcut\">Ctrl+A</span> pour sélectionner tous les modèles visibles."
|
||||
"content": "Activez le mode groupé en cliquant sur ce bouton ou en appuyant sur <span class=\"onboarding-shortcut\">B</span> pour sélectionner plusieurs modèles et effectuer des opérations groupées.<br>• <span class=\"onboarding-shortcut\">Ctrl/Cmd+A</span> sélectionne tous les modèles visibles, <span class=\"onboarding-shortcut\">Shift+Click</span> sélectionne une plage.<br>• <span class=\"onboarding-shortcut\">Esc</span> ou un clic sur une zone vide quitte le mode groupé."
|
||||
},
|
||||
"searchOptions": {
|
||||
"title": "Options de recherche",
|
||||
@@ -116,7 +116,19 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"title": "Menu contextuel",
|
||||
"content": "<strong>Clic droit</strong> sur une carte de modèle pour accéder à un menu contextuel avec des actions supplémentaires."
|
||||
"content": "<strong>Clic droit</strong> sur n'importe quelle carte de modèle pour ouvrir un menu contextuel avec des actions sur la carte comme déplacer, supprimer ou modifier les métadonnées."
|
||||
},
|
||||
"marqueeSelect": {
|
||||
"title": "Glisser pour sélectionner",
|
||||
"content": "Maintenez le <strong>bouton gauche de la souris</strong> enfoncé sur une zone vide de la grille et glissez pour tracer un rectangle de sélection qui sélectionne plusieurs cartes à la fois."
|
||||
},
|
||||
"dragToSidebar": {
|
||||
"title": "Organiser par glisser-déposer",
|
||||
"content": "Glissez une carte de modèle sur un dossier de la barre latérale pour y déplacer le fichier. Cela fonctionne aussi avec plusieurs cartes sélectionnées en mode groupé."
|
||||
},
|
||||
"contextMenus": {
|
||||
"title": "Plus de menus contextuels",
|
||||
"content": "En mode groupé, <strong>faites un clic droit sur une carte sélectionnée</strong> pour accéder aux actions groupées. <strong>Faites un clic droit sur une zone vide</strong> de la page pour accéder aux actions globales comme la vérification des mises à jour et la gestion des modèles exclus."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1629,7 +1641,7 @@
|
||||
"addPlaceholder": "Tapez pour ajouter ou cliquez sur les suggestions ci-dessous",
|
||||
"editWord": "Modifier le mot-clé",
|
||||
"editPlaceholder": "Modifier le mot-clé",
|
||||
"copyWord": "Copier le mot-clé",
|
||||
"copyOrEditWord": "Cliquez pour copier, double-cliquez pour modifier",
|
||||
"deleteWord": "Supprimer le mot-clé",
|
||||
"suggestions": {
|
||||
"noSuggestions": "Aucune suggestion disponible",
|
||||
@@ -1954,10 +1966,52 @@
|
||||
"tabs": {
|
||||
"gettingStarted": "Commencer",
|
||||
"updateVlogs": "Vlogs de mise à jour",
|
||||
"documentation": "Documentation"
|
||||
"documentation": "Documentation",
|
||||
"shortcuts": "Raccourcis"
|
||||
},
|
||||
"gettingStarted": {
|
||||
"title": "Premiers pas avec le Gestionnaire LoRA"
|
||||
"title": "Premiers pas avec le Gestionnaire LoRA",
|
||||
"replayTutorial": "Rejouer le tutoriel"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Raccourcis clavier et souris",
|
||||
"groups": {
|
||||
"general": "Général",
|
||||
"actions": "Actions",
|
||||
"selection": "Sélection et mode groupé",
|
||||
"navigation": "Navigation",
|
||||
"modelModal": "Modale Modèle / Recipe",
|
||||
"mediaViewer": "Visionneuse de médias / Galerie d'exemples"
|
||||
},
|
||||
"keys": {
|
||||
"click": "Clic",
|
||||
"drag": "Glisser",
|
||||
"rightClick": "Clic droit",
|
||||
"letter": "Lettre",
|
||||
"swipe": "Balayage"
|
||||
},
|
||||
"entries": {
|
||||
"focusSearch": "Donner le focus au champ de recherche",
|
||||
"closeModal": "Fermer la fenêtre modale / le panneau",
|
||||
"openShortcuts": "Ouvrir ce panneau de raccourcis",
|
||||
"refresh": "Actualiser la liste des modèles",
|
||||
"fetchMetadata": "Récupérer les métadonnées depuis CivitAI (pages de modèles uniquement)",
|
||||
"downloadModel": "Télécharger un modèle (pages de modèles uniquement)",
|
||||
"toggleBulkMode": "Activer/désactiver le mode groupé",
|
||||
"selectAll": "Sélectionner tous les modèles visibles",
|
||||
"rangeSelect": "Sélection d'une plage",
|
||||
"marqueeSelect": "Sélection par glisser-déposer des cartes (sur une zone vide de la grille)",
|
||||
"exitBulkMode": "Quitter le mode groupé",
|
||||
"bulkActions": "Sur une carte sélectionnée : menu des actions groupées",
|
||||
"globalActions": "Sur une zone vide de la page : menu des actions globales (vérification des mises à jour, gestion des modèles exclus)",
|
||||
"scrollPages": "Faire défiler les pages",
|
||||
"jumpAlphabet": "Sauter via la barre alphabétique",
|
||||
"prevNext": "Modèle précédent / suivant",
|
||||
"deleteEntry": "Supprimer",
|
||||
"cycleMedia": "Parcourir les médias ([ / ] dans la galerie d'exemples)",
|
||||
"swipeTouch": "Parcourir les médias sur les appareils tactiles",
|
||||
"closeViewer": "Fermer la visionneuse"
|
||||
}
|
||||
},
|
||||
"updateVlogs": {
|
||||
"title": "Dernières mises à jour",
|
||||
@@ -1974,7 +2028,8 @@
|
||||
"settings": "Paramètres & Configuration",
|
||||
"extensions": "Extensions",
|
||||
"newBadge": "NOUVEAU"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "NOUVEAU"
|
||||
},
|
||||
"update": {
|
||||
"title": "Vérifier les mises à jour",
|
||||
|
||||
+61
-6
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"bulk": {
|
||||
"title": "פעולות בכמות גדולה",
|
||||
"content": "היכנס למצב פעולות בכמות גדולה על ידי לחיצה על כפתור זה או על <span class=\"onboarding-shortcut\">B</span>. בחר מספר מודלים ובצע פעולות בכמות גדולה. השתמש ב-<span class=\"onboarding-shortcut\">Ctrl+A</span> כדי לבחור את כל המודלים הגלויים."
|
||||
"content": "היכנס למצב פעולות בכמות גדולה על ידי לחיצה על כפתור זה או על <span class=\"onboarding-shortcut\">B</span> כדי לבחור מספר מודלים ולבצע פעולות בכמות גדולה.<br>• <span class=\"onboarding-shortcut\">Ctrl/Cmd+A</span> בחר את כל המודלים הגלויים, <span class=\"onboarding-shortcut\">Shift+Click</span> בחר טווח.<br>• <span class=\"onboarding-shortcut\">Esc</span> או לחיצה על אזור ריק מוציאים ממצב בכמות גדולה."
|
||||
},
|
||||
"searchOptions": {
|
||||
"title": "אפשרויות חיפוש",
|
||||
@@ -116,7 +116,19 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"title": "תפריט הקשר",
|
||||
"content": "<strong>לחיצה ימנית</strong> על כל כרטיס מודל לתפריט הקשר עם פעולות נוספות."
|
||||
"content": "<strong>לחיצה ימנית</strong> על כל כרטיס מודל לתפריט הקשר עם פעולות כרטיס כמו העברה, מחיקה או עריכת מטא-נתונים."
|
||||
},
|
||||
"marqueeSelect": {
|
||||
"title": "גרור כדי לבחור",
|
||||
"content": "החזק את <strong>לחצן העכבר השמאלי</strong> לחוץ על אזור ריק של הרשת וגרור כדי לצייר מסגרת בחירה שבוחרת מספר כרטיסים בבת אחת."
|
||||
},
|
||||
"dragToSidebar": {
|
||||
"title": "ארגון באמצעות גרירה",
|
||||
"content": "גרור כרטיס מודל אל תיקייה בסרגל הצד כדי להעביר את הקובץ לשם. פעולה זו עובדת גם עם מספר כרטיסים נבחרים במצב בכמות גדולה."
|
||||
},
|
||||
"contextMenus": {
|
||||
"title": "תפריטי הקשר נוספים",
|
||||
"content": "במצב בכמות גדולה, <strong>לחץ לחיצה ימנית על כרטיס נבחר</strong> לפעולות בכמות גדולה. <strong>לחץ לחיצה ימנית על אזור ריק</strong> בדף לפעולות גלובליות כמו בדיקת עדכונים וניהול מודלים מוחרגים."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1629,7 +1641,7 @@
|
||||
"addPlaceholder": "הקלד להוספה או לחץ על הצעות למטה",
|
||||
"editWord": "עריכת מילת טריגר",
|
||||
"editPlaceholder": "עריכת מילת טריגר",
|
||||
"copyWord": "העתק מילת טריגר",
|
||||
"copyOrEditWord": "לחץ כדי להעתיק, לחץ פעמיים כדי לערוך",
|
||||
"deleteWord": "מחק מילת טריגר",
|
||||
"suggestions": {
|
||||
"noSuggestions": "אין הצעות זמינות",
|
||||
@@ -1954,10 +1966,52 @@
|
||||
"tabs": {
|
||||
"gettingStarted": "תחילת עבודה",
|
||||
"updateVlogs": "בלוגי וידאו של עדכונים",
|
||||
"documentation": "תיעוד"
|
||||
"documentation": "תיעוד",
|
||||
"shortcuts": "קיצורי דרך"
|
||||
},
|
||||
"gettingStarted": {
|
||||
"title": "תחילת עבודה עם מנהל LoRA"
|
||||
"title": "תחילת עבודה עם מנהל LoRA",
|
||||
"replayTutorial": "הפעל את המדריך מחדש"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "קיצורי מקלדת ועכבר",
|
||||
"groups": {
|
||||
"general": "כללי",
|
||||
"actions": "פעולות",
|
||||
"selection": "בחירה ומצב בכמות גדולה",
|
||||
"navigation": "ניווט",
|
||||
"modelModal": "חלון מודל / מתכון",
|
||||
"mediaViewer": "מציג מדיה / גלריית דוגמאות"
|
||||
},
|
||||
"keys": {
|
||||
"click": "לחיצה",
|
||||
"drag": "גרירה",
|
||||
"rightClick": "לחיצה ימנית",
|
||||
"letter": "אות",
|
||||
"swipe": "החלקה"
|
||||
},
|
||||
"entries": {
|
||||
"focusSearch": "העבר מיקוד לחיפוש",
|
||||
"closeModal": "סגור חלון / פאנל",
|
||||
"openShortcuts": "פתח את פאנל קיצורי הדרך הזה",
|
||||
"refresh": "רענן את רשימת המודלים",
|
||||
"fetchMetadata": "אחזר מטא-נתונים מ-CivitAI (דפי מודלים בלבד)",
|
||||
"downloadModel": "הורד מודל (דפי מודלים בלבד)",
|
||||
"toggleBulkMode": "הפעל/כבה מצב בכמות גדולה",
|
||||
"selectAll": "בחר את כל המודלים הגלויים",
|
||||
"rangeSelect": "בחר טווח",
|
||||
"marqueeSelect": "בחר כרטיסים במסגרת בחירה (באזור ריק של הרשת)",
|
||||
"exitBulkMode": "צא ממצב בכמות גדולה",
|
||||
"bulkActions": "על כרטיס נבחר: תפריט פעולות בכמות גדולה",
|
||||
"globalActions": "באזור ריק בדף: תפריט פעולות גלובליות (בדיקת עדכונים, ניהול מודלים מוחרגים)",
|
||||
"scrollPages": "גלול בין דפים",
|
||||
"jumpAlphabet": "קפוץ בעזרת סרגל האותיות",
|
||||
"prevNext": "מודל קודם / הבא",
|
||||
"deleteEntry": "מחק",
|
||||
"cycleMedia": "עבור בין פריטי מדיה ([ / ] בגלריית הדוגמאות)",
|
||||
"swipeTouch": "עבור בין פריטי מדיה במכשירי מגע",
|
||||
"closeViewer": "סגור את המציג"
|
||||
}
|
||||
},
|
||||
"updateVlogs": {
|
||||
"title": "עדכונים אחרונים",
|
||||
@@ -1974,7 +2028,8 @@
|
||||
"settings": "הגדרות ותצורה",
|
||||
"extensions": "הרחבות",
|
||||
"newBadge": "חדש"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "חדש"
|
||||
},
|
||||
"update": {
|
||||
"title": "בדוק עדכונים",
|
||||
|
||||
+61
-6
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"bulk": {
|
||||
"title": "一括操作",
|
||||
"content": "このボタンをクリックするか、<span class=\"onboarding-shortcut\">B</span>キーを押して一括モードに入ります。複数のモデルを選択して一括操作が可能です。<span class=\"onboarding-shortcut\">Ctrl+A</span>で表示中のモデルをすべて選択できます。"
|
||||
"content": "このボタンをクリックするか、<span class=\"onboarding-shortcut\">B</span>キーを押して一括モードに入り、複数のモデルを選択して一括操作を実行できます。<br>• <span class=\"onboarding-shortcut\">Ctrl/Cmd+A</span>で表示中のモデルをすべて選択、<span class=\"onboarding-shortcut\">Shift+Click</span>で範囲選択。<br>• <span class=\"onboarding-shortcut\">Esc</span>キーまたは空白部分をクリックすると一括モードを終了します。"
|
||||
},
|
||||
"searchOptions": {
|
||||
"title": "検索オプション",
|
||||
@@ -116,7 +116,19 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"title": "コンテキストメニュー",
|
||||
"content": "<strong>モデルカードを右クリック</strong>すると追加の操作ができるコンテキストメニューが表示されます。"
|
||||
"content": "<strong>モデルカードを右クリック</strong>すると、移動、削除、メタデータの編集などのカード操作を含むコンテキストメニューが表示されます。"
|
||||
},
|
||||
"marqueeSelect": {
|
||||
"title": "ドラッグで選択",
|
||||
"content": "グリッドの空白部分で<strong>マウスの左ボタン</strong>を押したままドラッグすると、複数のカードを一度に選択する矩形(マーキー)を描画できます。"
|
||||
},
|
||||
"dragToSidebar": {
|
||||
"title": "ドラッグで整理",
|
||||
"content": "モデルカードをサイドバーのフォルダにドラッグすると、ファイルをそこに移動できます。一括モードで複数選択したカードでも同様に機能します。"
|
||||
},
|
||||
"contextMenus": {
|
||||
"title": "その他のコンテキストメニュー",
|
||||
"content": "一括モードでは、<strong>選択したカードを右クリック</strong>すると一括操作メニューが表示されます。<strong>ページの空白部分を右クリック</strong>すると、更新の確認や除外モデルの管理などのグローバル操作メニューが表示されます。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1629,7 +1641,7 @@
|
||||
"addPlaceholder": "入力して追加するか、下の提案をクリック",
|
||||
"editWord": "トリガーワードを編集",
|
||||
"editPlaceholder": "トリガーワードを編集",
|
||||
"copyWord": "トリガーワードをコピー",
|
||||
"copyOrEditWord": "クリックでコピー、ダブルクリックで編集",
|
||||
"deleteWord": "トリガーワードを削除",
|
||||
"suggestions": {
|
||||
"noSuggestions": "提案はありません",
|
||||
@@ -1954,10 +1966,52 @@
|
||||
"tabs": {
|
||||
"gettingStarted": "はじめに",
|
||||
"updateVlogs": "更新Vlog",
|
||||
"documentation": "ドキュメント"
|
||||
"documentation": "ドキュメント",
|
||||
"shortcuts": "ショートカット"
|
||||
},
|
||||
"gettingStarted": {
|
||||
"title": "LoRA Managerを始める"
|
||||
"title": "LoRA Managerを始める",
|
||||
"replayTutorial": "チュートリアルをもう一度再生"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "キーボード & マウスのショートカット",
|
||||
"groups": {
|
||||
"general": "一般",
|
||||
"actions": "操作",
|
||||
"selection": "選択 & 一括モード",
|
||||
"navigation": "ナビゲーション",
|
||||
"modelModal": "モデル / レシピモーダル",
|
||||
"mediaViewer": "メディアビューア / ショーケース"
|
||||
},
|
||||
"keys": {
|
||||
"click": "クリック",
|
||||
"drag": "ドラッグ",
|
||||
"rightClick": "右クリック",
|
||||
"letter": "文字キー",
|
||||
"swipe": "スワイプ"
|
||||
},
|
||||
"entries": {
|
||||
"focusSearch": "検索にフォーカス",
|
||||
"closeModal": "モーダル / パネルを閉じる",
|
||||
"openShortcuts": "このショートカットパネルを開く",
|
||||
"refresh": "モデルリストを更新",
|
||||
"fetchMetadata": "CivitAIからメタデータを取得(モデルページのみ)",
|
||||
"downloadModel": "モデルをダウンロード(モデルページのみ)",
|
||||
"toggleBulkMode": "一括モードを切り替え",
|
||||
"selectAll": "表示中のモデルをすべて選択",
|
||||
"rangeSelect": "範囲選択",
|
||||
"marqueeSelect": "カードを矩形選択(グリッドの空白部分で)",
|
||||
"exitBulkMode": "一括モードを終了",
|
||||
"bulkActions": "選択したカード上:一括操作メニュー",
|
||||
"globalActions": "ページの空白部分:グローバル操作メニュー(更新の確認、除外モデルの管理)",
|
||||
"scrollPages": "ページをスクロール",
|
||||
"jumpAlphabet": "アルファベットバーへジャンプ",
|
||||
"prevNext": "前 / 次のモデル",
|
||||
"deleteEntry": "削除",
|
||||
"cycleMedia": "メディアを切り替え(ショーケースギャラリーでは [ / ])",
|
||||
"swipeTouch": "タッチデバイスでメディアを切り替え",
|
||||
"closeViewer": "ビューアを閉じる"
|
||||
}
|
||||
},
|
||||
"updateVlogs": {
|
||||
"title": "最新の更新",
|
||||
@@ -1974,7 +2028,8 @@
|
||||
"settings": "設定&構成",
|
||||
"extensions": "拡張機能",
|
||||
"newBadge": "新着"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "新着"
|
||||
},
|
||||
"update": {
|
||||
"title": "更新確認",
|
||||
|
||||
+61
-6
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"bulk": {
|
||||
"title": "일괄 작업",
|
||||
"content": "이 버튼을 클릭하거나 <span class=\"onboarding-shortcut\">B</span> 키를 눌러 일괄 모드로 진입하세요. 여러 모델을 선택하여 일괄 작업을 수행할 수 있습니다. <span class=\"onboarding-shortcut\">Ctrl+A</span>로 모든 표시된 모델을 선택하세요."
|
||||
"content": "이 버튼을 클릭하거나 <span class=\"onboarding-shortcut\">B</span> 키를 눌러 일괄 모드로 진입하여 여러 모델을 선택하고 일괄 작업을 수행하세요.<br>• <span class=\"onboarding-shortcut\">Ctrl/Cmd+A</span>로 모든 표시된 모델을 선택하고, <span class=\"onboarding-shortcut\">Shift+Click</span>으로 범위를 선택할 수 있습니다.<br>• <span class=\"onboarding-shortcut\">Esc</span> 키를 누르거나 빈 영역을 클릭하면 일괄 모드가 종료됩니다."
|
||||
},
|
||||
"searchOptions": {
|
||||
"title": "검색 옵션",
|
||||
@@ -116,7 +116,19 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"title": "컨텍스트 메뉴",
|
||||
"content": "<strong>오른쪽 클릭</strong>으로 모델 카드의 추가 작업 메뉴를 사용할 수 있습니다."
|
||||
"content": "모델 카드를 <strong>오른쪽 클릭</strong>하면 이동, 삭제, 메타데이터 편집 같은 카드 작업이 담긴 컨텍스트 메뉴를 사용할 수 있습니다."
|
||||
},
|
||||
"marqueeSelect": {
|
||||
"title": "드래그로 선택",
|
||||
"content": "그리드의 빈 영역에서 <strong>마우스 왼쪽 버튼</strong>을 누른 채 드래그하여 여러 카드를 한 번에 선택하는 선택 영역을 그리세요."
|
||||
},
|
||||
"dragToSidebar": {
|
||||
"title": "드래그로 정리",
|
||||
"content": "모델 카드를 사이드바의 폴더로 드래그하면 파일이 해당 폴더로 이동합니다. 일괄 모드에서 선택한 여러 카드에도 적용됩니다."
|
||||
},
|
||||
"contextMenus": {
|
||||
"title": "더 많은 컨텍스트 메뉴",
|
||||
"content": "일괄 모드에서는 <strong>선택한 카드를 오른쪽 클릭</strong>하여 일괄 작업을 사용할 수 있습니다. 페이지의 <strong>빈 영역을 오른쪽 클릭</strong>하면 업데이트 확인이나 제외된 모델 관리 같은 전역 작업을 사용할 수 있습니다."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1629,7 +1641,7 @@
|
||||
"addPlaceholder": "입력하거나 아래 제안을 클릭하세요",
|
||||
"editWord": "트리거 단어 편집",
|
||||
"editPlaceholder": "트리거 단어 편집",
|
||||
"copyWord": "트리거 단어 복사",
|
||||
"copyOrEditWord": "클릭하여 복사, 더블 클릭하여 편집",
|
||||
"deleteWord": "트리거 단어 삭제",
|
||||
"suggestions": {
|
||||
"noSuggestions": "사용 가능한 제안이 없습니다",
|
||||
@@ -1954,10 +1966,52 @@
|
||||
"tabs": {
|
||||
"gettingStarted": "시작하기",
|
||||
"updateVlogs": "업데이트 영상",
|
||||
"documentation": "문서"
|
||||
"documentation": "문서",
|
||||
"shortcuts": "단축키"
|
||||
},
|
||||
"gettingStarted": {
|
||||
"title": "LoRA Manager 시작하기"
|
||||
"title": "LoRA Manager 시작하기",
|
||||
"replayTutorial": "튜토리얼 다시 보기"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "키보드 & 마우스 단축키",
|
||||
"groups": {
|
||||
"general": "일반",
|
||||
"actions": "작업",
|
||||
"selection": "선택 & 일괄 모드",
|
||||
"navigation": "내비게이션",
|
||||
"modelModal": "모델 / 레시피 모달",
|
||||
"mediaViewer": "미디어 뷰어 / 쇼케이스"
|
||||
},
|
||||
"keys": {
|
||||
"click": "클릭",
|
||||
"drag": "드래그",
|
||||
"rightClick": "오른쪽 클릭",
|
||||
"letter": "문자 키",
|
||||
"swipe": "스와이프"
|
||||
},
|
||||
"entries": {
|
||||
"focusSearch": "검색창으로 포커스 이동",
|
||||
"closeModal": "모달 / 패널 닫기",
|
||||
"openShortcuts": "이 단축키 패널 열기",
|
||||
"refresh": "모델 목록 새로고침",
|
||||
"fetchMetadata": "CivitAI에서 메타데이터 가져오기 (모델 페이지만)",
|
||||
"downloadModel": "모델 다운로드 (모델 페이지만)",
|
||||
"toggleBulkMode": "일괄 모드 전환",
|
||||
"selectAll": "표시된 모든 모델 선택",
|
||||
"rangeSelect": "범위 선택",
|
||||
"marqueeSelect": "드래그로 카드 선택 (빈 그리드 영역에서)",
|
||||
"exitBulkMode": "일괄 모드 종료",
|
||||
"bulkActions": "선택한 카드에서: 일괄 작업 메뉴",
|
||||
"globalActions": "페이지 빈 영역에서: 전역 작업 메뉴 (업데이트 확인, 제외된 모델 관리)",
|
||||
"scrollPages": "페이지 스크롤",
|
||||
"jumpAlphabet": "알파벳 바로 이동",
|
||||
"prevNext": "이전 / 다음 모델",
|
||||
"deleteEntry": "삭제",
|
||||
"cycleMedia": "미디어 전환 (쇼케이스 갤러리에서 [ / ])",
|
||||
"swipeTouch": "터치 기기에서 미디어 전환",
|
||||
"closeViewer": "뷰어 닫기"
|
||||
}
|
||||
},
|
||||
"updateVlogs": {
|
||||
"title": "최신 업데이트",
|
||||
@@ -1974,7 +2028,8 @@
|
||||
"settings": "설정 & 구성",
|
||||
"extensions": "확장",
|
||||
"newBadge": "신규"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "신규"
|
||||
},
|
||||
"update": {
|
||||
"title": "업데이트 확인",
|
||||
|
||||
+61
-6
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"bulk": {
|
||||
"title": "Массовые операции",
|
||||
"content": "Войдите в массовый режим, нажав эту кнопку или клавишу <span class=\"onboarding-shortcut\">B</span>. Выберите несколько моделей и выполните пакетные операции. Используйте <span class=\"onboarding-shortcut\">Ctrl+A</span> для выбора всех видимых моделей."
|
||||
"content": "Войдите в массовый режим, нажав эту кнопку или клавишу <span class=\"onboarding-shortcut\">B</span>, чтобы выбрать несколько моделей и выполнить пакетные операции.<br>• <span class=\"onboarding-shortcut\">Ctrl/Cmd+A</span> — выбрать все видимые модели, <span class=\"onboarding-shortcut\">Shift+Click</span> — выбрать диапазон.<br>• <span class=\"onboarding-shortcut\">Esc</span> или клик по пустой области выходит из массового режима."
|
||||
},
|
||||
"searchOptions": {
|
||||
"title": "Опции поиска",
|
||||
@@ -116,7 +116,19 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"title": "Контекстное меню",
|
||||
"content": "<strong>Правый клик</strong> по карточке модели откроет контекстное меню с дополнительными действиями."
|
||||
"content": "<strong>Правый клик</strong> по любой карточке модели открывает контекстное меню с действиями над карточкой, такими как перемещение, удаление или редактирование метаданных."
|
||||
},
|
||||
"marqueeSelect": {
|
||||
"title": "Выделение рамкой",
|
||||
"content": "Удерживайте <strong>левую кнопку мыши</strong> на пустой области сетки и перетащите, чтобы нарисовать рамку, выделяющую сразу несколько карточек."
|
||||
},
|
||||
"dragToSidebar": {
|
||||
"title": "Организация перетаскиванием",
|
||||
"content": "Перетащите карточку модели на папку в боковой панели, чтобы переместить туда файл. Это также работает с несколькими выделенными карточками в массовом режиме."
|
||||
},
|
||||
"contextMenus": {
|
||||
"title": "Другие контекстные меню",
|
||||
"content": "В массовом режиме <strong>правый клик по выделенной карточке</strong> открывает меню массовых операций. <strong>Правый клик по пустой области</strong> страницы открывает глобальные действия, такие как проверка обновлений и управление исключёнными моделями."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1629,7 +1641,7 @@
|
||||
"addPlaceholder": "Введите для добавления или нажмите на предложения ниже",
|
||||
"editWord": "Редактировать триггерное слово",
|
||||
"editPlaceholder": "Редактировать триггерное слово",
|
||||
"copyWord": "Копировать триггерное слово",
|
||||
"copyOrEditWord": "Клик — скопировать, двойной клик — редактировать",
|
||||
"deleteWord": "Удалить триггерное слово",
|
||||
"suggestions": {
|
||||
"noSuggestions": "Предложения недоступны",
|
||||
@@ -1954,10 +1966,52 @@
|
||||
"tabs": {
|
||||
"gettingStarted": "Начало работы",
|
||||
"updateVlogs": "Видео обновлений",
|
||||
"documentation": "Документация"
|
||||
"documentation": "Документация",
|
||||
"shortcuts": "Горячие клавиши"
|
||||
},
|
||||
"gettingStarted": {
|
||||
"title": "Начало работы с LoRA Manager"
|
||||
"title": "Начало работы с LoRA Manager",
|
||||
"replayTutorial": "Повторить обучение"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Горячие клавиши и действия мыши",
|
||||
"groups": {
|
||||
"general": "Общие",
|
||||
"actions": "Действия",
|
||||
"selection": "Выделение и массовый режим",
|
||||
"navigation": "Навигация",
|
||||
"modelModal": "Окно модели / рецепта",
|
||||
"mediaViewer": "Просмотр медиа / Витрина"
|
||||
},
|
||||
"keys": {
|
||||
"click": "Клик",
|
||||
"drag": "Перетаскивание",
|
||||
"rightClick": "Правый клик",
|
||||
"letter": "Буква",
|
||||
"swipe": "Свайп"
|
||||
},
|
||||
"entries": {
|
||||
"focusSearch": "Переход к поиску",
|
||||
"closeModal": "Закрыть модальное окно / панель",
|
||||
"openShortcuts": "Открыть эту панель горячих клавиш",
|
||||
"refresh": "Обновить список моделей",
|
||||
"fetchMetadata": "Получить метаданные с CivitAI (только на страницах моделей)",
|
||||
"downloadModel": "Загрузить модель (только на страницах моделей)",
|
||||
"toggleBulkMode": "Переключить массовый режим",
|
||||
"selectAll": "Выбрать все видимые модели",
|
||||
"rangeSelect": "Выбор диапазона",
|
||||
"marqueeSelect": "Выделение карточек рамкой (на пустой области сетки)",
|
||||
"exitBulkMode": "Выйти из массового режима",
|
||||
"bulkActions": "На выделенной карточке: меню массовых операций",
|
||||
"globalActions": "На пустой области страницы: меню глобальных действий (проверка обновлений, управление исключёнными моделями)",
|
||||
"scrollPages": "Прокрутка страниц",
|
||||
"jumpAlphabet": "Переход по алфавитной панели",
|
||||
"prevNext": "Предыдущая / следующая модель",
|
||||
"deleteEntry": "Удалить",
|
||||
"cycleMedia": "Переключение медиа ([ / ] в галерее витрины)",
|
||||
"swipeTouch": "Переключение медиа на сенсорных устройствах",
|
||||
"closeViewer": "Закрыть окно просмотра"
|
||||
}
|
||||
},
|
||||
"updateVlogs": {
|
||||
"title": "Последние обновления",
|
||||
@@ -1974,7 +2028,8 @@
|
||||
"settings": "Настройки и конфигурация",
|
||||
"extensions": "Расширения",
|
||||
"newBadge": "НОВОЕ"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "НОВОЕ"
|
||||
},
|
||||
"update": {
|
||||
"title": "Проверить обновления",
|
||||
|
||||
+61
-6
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"bulk": {
|
||||
"title": "批量操作",
|
||||
"content": "点击此按钮或按 <span class=\"onboarding-shortcut\">B</span> 进入批量模式。可多选模型并进行批量操作。使用 <span class=\"onboarding-shortcut\">Ctrl+A</span> 全选所有可见模型。"
|
||||
"content": "点击此按钮或按 <span class=\"onboarding-shortcut\">B</span> 进入批量模式,可多选模型并执行批量操作。<br>• <span class=\"onboarding-shortcut\">Ctrl/Cmd+A</span> 全选所有可见模型,<span class=\"onboarding-shortcut\">Shift+Click</span> 选择一个范围。<br>• 按 <span class=\"onboarding-shortcut\">Esc</span> 或点击空白区域退出批量模式。"
|
||||
},
|
||||
"searchOptions": {
|
||||
"title": "搜索选项",
|
||||
@@ -116,7 +116,19 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"title": "右键菜单",
|
||||
"content": "<strong>右键点击</strong>任意模型卡片可打开更多操作菜单。"
|
||||
"content": "<strong>右键点击</strong>任意模型卡片,可打开包含移动、删除或编辑元数据等卡片操作的菜单。"
|
||||
},
|
||||
"marqueeSelect": {
|
||||
"title": "拖动框选",
|
||||
"content": "在网格的空白区域按住<strong>鼠标左键</strong>并拖动,绘制一个可同时选中多张卡片的框选区域。"
|
||||
},
|
||||
"dragToSidebar": {
|
||||
"title": "拖放整理",
|
||||
"content": "将模型卡片拖到侧边栏的文件夹上,即可把文件移动到该文件夹。批量模式下选中的多张卡片也可如此操作。"
|
||||
},
|
||||
"contextMenus": {
|
||||
"title": "更多右键菜单",
|
||||
"content": "在批量模式下,<strong>右键点击已选中的卡片</strong>可进行批量操作。<strong>右键点击页面空白区域</strong>可使用检查更新、管理已排除的模型等全局操作。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1629,7 +1641,7 @@
|
||||
"addPlaceholder": "输入或点击下方建议添加",
|
||||
"editWord": "编辑触发词",
|
||||
"editPlaceholder": "编辑触发词",
|
||||
"copyWord": "复制触发词",
|
||||
"copyOrEditWord": "单击复制,双击编辑",
|
||||
"deleteWord": "删除触发词",
|
||||
"suggestions": {
|
||||
"noSuggestions": "暂无建议",
|
||||
@@ -1954,10 +1966,52 @@
|
||||
"tabs": {
|
||||
"gettingStarted": "新手入门",
|
||||
"updateVlogs": "更新日志",
|
||||
"documentation": "文档"
|
||||
"documentation": "文档",
|
||||
"shortcuts": "快捷键"
|
||||
},
|
||||
"gettingStarted": {
|
||||
"title": "LoRA 管理器新手入门"
|
||||
"title": "LoRA 管理器新手入门",
|
||||
"replayTutorial": "重播教程"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "键盘与鼠标快捷键",
|
||||
"groups": {
|
||||
"general": "通用",
|
||||
"actions": "操作",
|
||||
"selection": "选择与批量模式",
|
||||
"navigation": "导航",
|
||||
"modelModal": "模型 / 配方弹窗",
|
||||
"mediaViewer": "媒体查看器 / 示例展示"
|
||||
},
|
||||
"keys": {
|
||||
"click": "单击",
|
||||
"drag": "拖动",
|
||||
"rightClick": "右键点击",
|
||||
"letter": "字母",
|
||||
"swipe": "滑动"
|
||||
},
|
||||
"entries": {
|
||||
"focusSearch": "聚焦搜索框",
|
||||
"closeModal": "关闭弹窗 / 面板",
|
||||
"openShortcuts": "打开本快捷键面板",
|
||||
"refresh": "刷新模型列表",
|
||||
"fetchMetadata": "从 CivitAI 获取元数据(仅模型页面)",
|
||||
"downloadModel": "下载模型(仅模型页面)",
|
||||
"toggleBulkMode": "切换批量模式",
|
||||
"selectAll": "全选所有可见模型",
|
||||
"rangeSelect": "范围选择",
|
||||
"marqueeSelect": "框选卡片(在网格空白区域)",
|
||||
"exitBulkMode": "退出批量模式",
|
||||
"bulkActions": "在已选中的卡片上:批量操作菜单",
|
||||
"globalActions": "在页面空白区域:全局操作菜单(检查更新、管理已排除的模型)",
|
||||
"scrollPages": "滚动页面",
|
||||
"jumpAlphabet": "字母索引栏跳转",
|
||||
"prevNext": "上一个 / 下一个模型",
|
||||
"deleteEntry": "删除",
|
||||
"cycleMedia": "切换媒体(在示例展示中按 [ / ])",
|
||||
"swipeTouch": "在触屏设备上切换媒体",
|
||||
"closeViewer": "关闭查看器"
|
||||
}
|
||||
},
|
||||
"updateVlogs": {
|
||||
"title": "最新更新",
|
||||
@@ -1974,7 +2028,8 @@
|
||||
"settings": "设置与配置",
|
||||
"extensions": "扩展",
|
||||
"newBadge": "新"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "新"
|
||||
},
|
||||
"update": {
|
||||
"title": "检查更新",
|
||||
|
||||
+61
-6
@@ -96,7 +96,7 @@
|
||||
},
|
||||
"bulk": {
|
||||
"title": "批次操作",
|
||||
"content": "點擊此按鈕或按下 <span class=\"onboarding-shortcut\">B</span> 進入批次模式。可選取多個模型並執行批量操作。使用 <span class=\"onboarding-shortcut\">Ctrl+A</span> 選取所有可見模型。"
|
||||
"content": "點擊此按鈕或按下 <span class=\"onboarding-shortcut\">B</span> 進入批量模式,選取多個模型並執行批量操作。<br>• <span class=\"onboarding-shortcut\">Ctrl/Cmd+A</span> 選取所有可見模型,<span class=\"onboarding-shortcut\">Shift+Click</span> 選取一段範圍。<br>• <span class=\"onboarding-shortcut\">Esc</span> 或點擊空白處離開批量模式。"
|
||||
},
|
||||
"searchOptions": {
|
||||
"title": "搜尋選項",
|
||||
@@ -116,7 +116,19 @@
|
||||
},
|
||||
"contextMenu": {
|
||||
"title": "右鍵選單",
|
||||
"content": "<strong>右鍵點擊</strong>任一模型卡片可開啟更多操作選單。"
|
||||
"content": "<strong>右鍵點擊</strong>任一模型卡片,可開啟包含移動、刪除或編輯中繼資料等卡片操作的右鍵選單。"
|
||||
},
|
||||
"marqueeSelect": {
|
||||
"title": "拖曳框選",
|
||||
"content": "在網格空白處按住<strong>滑鼠左鍵</strong>並拖曳,畫出框選範圍,一次選取多張卡片。"
|
||||
},
|
||||
"dragToSidebar": {
|
||||
"title": "拖曳整理",
|
||||
"content": "將模型卡片拖曳到側邊欄的資料夾上,即可將檔案移動到該處。在批量模式下選取多張卡片也可一起拖曳。"
|
||||
},
|
||||
"contextMenus": {
|
||||
"title": "更多右鍵選單",
|
||||
"content": "在批量模式下,<strong>右鍵點擊已選取的卡片</strong>可開啟批量操作選單。<strong>右鍵點擊頁面空白處</strong>可開啟全域操作選單,例如檢查更新與管理已排除的模型。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1629,7 +1641,7 @@
|
||||
"addPlaceholder": "輸入或點擊下方建議",
|
||||
"editWord": "編輯觸發詞",
|
||||
"editPlaceholder": "編輯觸發詞",
|
||||
"copyWord": "複製觸發詞",
|
||||
"copyOrEditWord": "點擊複製,雙擊編輯",
|
||||
"deleteWord": "刪除觸發詞",
|
||||
"suggestions": {
|
||||
"noSuggestions": "無可用建議",
|
||||
@@ -1954,10 +1966,52 @@
|
||||
"tabs": {
|
||||
"gettingStarted": "快速開始",
|
||||
"updateVlogs": "更新影片",
|
||||
"documentation": "文件"
|
||||
"documentation": "文件",
|
||||
"shortcuts": "快捷鍵"
|
||||
},
|
||||
"gettingStarted": {
|
||||
"title": "LoRA 管理器快速開始"
|
||||
"title": "LoRA 管理器快速開始",
|
||||
"replayTutorial": "重新播放教學"
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "鍵盤與滑鼠快捷鍵",
|
||||
"groups": {
|
||||
"general": "一般",
|
||||
"actions": "操作",
|
||||
"selection": "選取與批量模式",
|
||||
"navigation": "導覽",
|
||||
"modelModal": "模型 / 配方彈窗",
|
||||
"mediaViewer": "媒體檢視器 / 範例展示"
|
||||
},
|
||||
"keys": {
|
||||
"click": "點擊",
|
||||
"drag": "拖曳",
|
||||
"rightClick": "右鍵點擊",
|
||||
"letter": "字母鍵",
|
||||
"swipe": "滑動"
|
||||
},
|
||||
"entries": {
|
||||
"focusSearch": "聚焦搜尋欄",
|
||||
"closeModal": "關閉彈窗 / 面板",
|
||||
"openShortcuts": "開啟此快捷鍵面板",
|
||||
"refresh": "重新整理模型列表",
|
||||
"fetchMetadata": "從 CivitAI 擷取中繼資料(僅限模型頁面)",
|
||||
"downloadModel": "下載模型(僅限模型頁面)",
|
||||
"toggleBulkMode": "切換批量模式",
|
||||
"selectAll": "選取所有可見模型",
|
||||
"rangeSelect": "範圍選取",
|
||||
"marqueeSelect": "框選卡片(在網格空白處拖曳)",
|
||||
"exitBulkMode": "離開批量模式",
|
||||
"bulkActions": "在已選取的卡片上:批量操作選單",
|
||||
"globalActions": "在頁面空白處:全域操作選單(檢查更新、管理已排除的模型)",
|
||||
"scrollPages": "捲動頁面",
|
||||
"jumpAlphabet": "字母列跳轉",
|
||||
"prevNext": "上一個 / 下一個模型",
|
||||
"deleteEntry": "刪除",
|
||||
"cycleMedia": "切換媒體(範例展示中的 [ / ])",
|
||||
"swipeTouch": "在觸控裝置上滑動切換媒體",
|
||||
"closeViewer": "關閉檢視器"
|
||||
}
|
||||
},
|
||||
"updateVlogs": {
|
||||
"title": "最新更新",
|
||||
@@ -1974,7 +2028,8 @@
|
||||
"settings": "設定與配置",
|
||||
"extensions": "擴充功能",
|
||||
"newBadge": "新"
|
||||
}
|
||||
},
|
||||
"newContentBadge": "新"
|
||||
},
|
||||
"update": {
|
||||
"title": "檢查更新",
|
||||
|
||||
+8
-3
@@ -31,9 +31,14 @@ body {
|
||||
--header-height: 48px;
|
||||
--scrollbar-width: 8px;
|
||||
|
||||
--shortcut-bg: var(--color-accent-subtle);
|
||||
--shortcut-border: var(--color-accent-border);
|
||||
--shortcut-text: var(--text-primary);
|
||||
/* Neutral "keycap" style for keyboard shortcut hints (GitHub/Linear-like).
|
||||
Derived from --text-muted so it adapts to every theme/preset. */
|
||||
--shortcut-bg: color-mix(in oklch, var(--text-muted) 10%, transparent);
|
||||
--shortcut-bg-hover: color-mix(in oklch, var(--text-muted) 16%, transparent);
|
||||
--shortcut-border: color-mix(in oklch, var(--text-muted) 30%, transparent);
|
||||
--shortcut-border-hover: color-mix(in oklch, var(--text-muted) 45%, transparent);
|
||||
--shortcut-text: var(--text-muted);
|
||||
--shortcut-shadow: 0 1.5px 0 color-mix(in oklch, var(--text-muted) 30%, transparent);
|
||||
|
||||
--lora-accent-transparent: var(--color-accent-transparent);
|
||||
|
||||
|
||||
@@ -249,10 +249,10 @@
|
||||
font-family: inherit;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
/* Subtle tint derived from text color so it adapts to both light & dark themes */
|
||||
background: color-mix(in oklch, var(--text-muted) 12%, transparent);
|
||||
border: 1px solid color-mix(in oklch, var(--text-muted) 25%, transparent);
|
||||
color: var(--shortcut-text);
|
||||
background: var(--shortcut-bg);
|
||||
border: 1px solid var(--shortcut-border);
|
||||
box-shadow: var(--shortcut-shadow);
|
||||
border-radius: var(--border-radius-xs, 3px);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -167,6 +167,29 @@
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
/* Replay Tutorial button: badge hidden until the button is flagged as new content */
|
||||
.replay-tutorial-btn .new-content-badge {
|
||||
display: none;
|
||||
background-color: rgba(255, 255, 255, 0.22);
|
||||
color: #fff;
|
||||
box-shadow: none;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.replay-tutorial-btn.has-new-content .new-content-badge {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
/* One-time attention pulse when the button is flagged as new content */
|
||||
@keyframes new-content-glow {
|
||||
0% { box-shadow: 0 0 0 0 oklch(from var(--lora-accent) l c h / 55%); }
|
||||
100% { box-shadow: 0 0 0 16px transparent; }
|
||||
}
|
||||
|
||||
.replay-tutorial-btn.has-new-content {
|
||||
animation: new-content-glow 1.2s ease-out 3;
|
||||
}
|
||||
|
||||
/* Update video list styles */
|
||||
.video-list {
|
||||
display: flex;
|
||||
@@ -304,4 +327,87 @@
|
||||
/* Dark theme adjustments */
|
||||
[data-theme="dark"] .video-container {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
}
|
||||
/* Replay tutorial button styles */
|
||||
.help-actions {
|
||||
margin-top: var(--space-3);
|
||||
}
|
||||
|
||||
.replay-tutorial-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border-radius: var(--border-radius-sm);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: var(--transition-base);
|
||||
background-color: var(--lora-accent);
|
||||
color: white;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.replay-tutorial-btn:hover {
|
||||
background-color: oklch(from var(--lora-accent) l c h / 85%);
|
||||
}
|
||||
|
||||
/* Shortcuts tab styles */
|
||||
.shortcuts-section {
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.shortcuts-section h4 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
.shortcuts-list {
|
||||
list-style-type: none;
|
||||
padding-left: var(--space-3);
|
||||
}
|
||||
|
||||
.shortcuts-list li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: var(--space-1);
|
||||
}
|
||||
|
||||
.shortcut-keys {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.shortcut-sep {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.75rem;
|
||||
margin: 0 1px;
|
||||
}
|
||||
|
||||
.shortcuts-list kbd {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 4px;
|
||||
font-family: inherit;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 500;
|
||||
color: var(--shortcut-text);
|
||||
background: var(--shortcut-bg);
|
||||
border: 1px solid var(--shortcut-border);
|
||||
box-shadow: var(--shortcut-shadow);
|
||||
border-radius: var(--border-radius-xs, 3px);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.shortcut-description {
|
||||
font-size: 0.9em;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
@@ -202,15 +202,17 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-left: 6px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 3px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
border-radius: var(--border-radius-xs);
|
||||
background-color: var(--shortcut-bg);
|
||||
border: 1px solid var(--shortcut-border);
|
||||
box-shadow: var(--shortcut-shadow);
|
||||
color: var(--shortcut-text);
|
||||
vertical-align: middle;
|
||||
opacity: 0.8;
|
||||
@@ -219,12 +221,8 @@
|
||||
|
||||
.control-group button:hover .shortcut-key {
|
||||
opacity: 1;
|
||||
background-color: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.2);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .shortcut-key {
|
||||
--shortcut-bg: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.15);
|
||||
--shortcut-border: oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.3);
|
||||
background-color: var(--shortcut-bg-hover);
|
||||
border-color: var(--shortcut-border-hover);
|
||||
}
|
||||
|
||||
/* Ensure correct vertical alignment for text+shortcut */
|
||||
|
||||
@@ -205,6 +205,7 @@
|
||||
display: inline-block;
|
||||
background: var(--shortcut-bg);
|
||||
border: 1px solid var(--shortcut-border);
|
||||
box-shadow: var(--shortcut-shadow);
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding: 2px 6px;
|
||||
font-size: 0.8em;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// PageControls.js - Manages controls for both LoRAs and Checkpoints pages
|
||||
import { state, getCurrentPageState, setCurrentPageType } from '../../state/index.js';
|
||||
import { getStorageItem, setStorageItem, removeStorageItem, getSessionItem, setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
|
||||
import { showToast, openCivitaiByMetadata } from '../../utils/uiHelpers.js';
|
||||
import { showToast, openCivitaiByMetadata, isTypingContext } from '../../utils/uiHelpers.js';
|
||||
import { eventManager } from '../../utils/EventManager.js';
|
||||
import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js';
|
||||
import { sidebarManager } from '../SidebarManager.js';
|
||||
import { initSortDropdown, applySortToSelect, randomizeSortValue } from './SortDropdown.js';
|
||||
@@ -146,6 +147,62 @@ export class PageControls {
|
||||
|
||||
// Page-specific event listeners
|
||||
this.initPageSpecificListeners();
|
||||
|
||||
// Keyboard shortcuts for the actions toolbar (R / F / D)
|
||||
this.registerKeyboardShortcuts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register keyboard shortcuts for the actions toolbar buttons
|
||||
* (R = refresh, F = fetch metadata, D = download)
|
||||
*/
|
||||
registerKeyboardShortcuts() {
|
||||
eventManager.addHandler('keydown', 'pageControls-actions', (e) => {
|
||||
return this.handleActionShortcut(e);
|
||||
}, {
|
||||
priority: 90,
|
||||
skipWhenModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a keydown event for the actions toolbar shortcuts
|
||||
* @param {KeyboardEvent} e
|
||||
* @returns {boolean} True when the event was handled and propagation should stop
|
||||
*/
|
||||
handleActionShortcut(e) {
|
||||
// Plain letters only — leave modified combos (Ctrl/Cmd/Alt) alone
|
||||
if (e.ctrlKey || e.metaKey || e.altKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't hijack keys while typing in a text entry context
|
||||
if (isTypingContext(e.target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const actionByKey = {
|
||||
r: 'refresh',
|
||||
f: 'fetch',
|
||||
d: 'download'
|
||||
};
|
||||
const action = actionByKey[e.key.toLowerCase()];
|
||||
if (!action) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The button may not exist on this page (e.g. recipes has no
|
||||
// fetch/download) — let other handlers run in that case
|
||||
const button = document.querySelector(`[data-action="${action}"]`);
|
||||
if (!button) {
|
||||
return false;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
// Native disabled buttons ignore .click(), so an in-progress
|
||||
// refresh is safe
|
||||
button.click();
|
||||
return true;
|
||||
}
|
||||
|
||||
initExcludedViewControls() {
|
||||
|
||||
@@ -607,9 +607,11 @@ export function createModelCard(model, modelType) {
|
||||
sendTitle = translate('modelCard.actions.sendToWorkflow', {}, 'Send to ComfyUI (Click: Append, Shift+Click: Replace)');
|
||||
copyTitle = translate('modelCard.actions.copyLoRASyntax', {}, 'Copy LoRA Syntax');
|
||||
} else if (modelType === MODEL_TYPES.CHECKPOINT) {
|
||||
// Checkpoint send sets the widget value directly; no append/replace modes.
|
||||
sendTitle = translate('modelCard.actions.sendCheckpointToWorkflow', {}, 'Send to ComfyUI');
|
||||
copyTitle = translate('modelCard.actions.copyCheckpointName', {}, 'Copy checkpoint name');
|
||||
} else if (modelType === MODEL_TYPES.EMBEDDING) {
|
||||
// Embedding send always appends to the prompt; no replace mode.
|
||||
sendTitle = translate('modelCard.actions.sendEmbeddingToWorkflow', {}, 'Send to ComfyUI');
|
||||
copyTitle = translate('modelCard.actions.copyEmbeddingName', {}, 'Copy embedding name');
|
||||
} else {
|
||||
|
||||
@@ -227,7 +227,7 @@ export function renderTriggerWords(words, filePath) {
|
||||
const escapedWord = escapeHtml(word);
|
||||
const escapedAttr = escapeAttribute(word);
|
||||
return `
|
||||
<div class="trigger-word-tag" data-word="${escapedAttr}" title="${translate('modals.model.triggerWords.copyWord')}">
|
||||
<div class="trigger-word-tag" data-word="${escapedAttr}" title="${translate('modals.model.triggerWords.copyOrEditWord')}">
|
||||
<span class="trigger-word-content">${escapedWord}</span>
|
||||
<span class="trigger-word-copy">
|
||||
<i class="fas fa-copy"></i>
|
||||
@@ -455,7 +455,7 @@ function resetTriggerWordsUIState(section) {
|
||||
// Restore click-to-copy functionality
|
||||
tag.removeEventListener('click', startEditTriggerWord);
|
||||
setupDisplayTriggerWordTag(tag);
|
||||
tag.title = translate('modals.model.triggerWords.copyWord');
|
||||
tag.title = translate('modals.model.triggerWords.copyOrEditWord');
|
||||
|
||||
// Show copy icon, hide delete button
|
||||
if (copyIcon) copyIcon.style.display = '';
|
||||
@@ -503,7 +503,7 @@ function createTriggerWordTag(word, isEditMode = false) {
|
||||
const tag = document.createElement('div');
|
||||
tag.className = 'trigger-word-tag';
|
||||
tag.dataset.word = word;
|
||||
tag.title = translate(isEditMode ? 'modals.model.triggerWords.editWord' : 'modals.model.triggerWords.copyWord');
|
||||
tag.title = translate(isEditMode ? 'modals.model.triggerWords.editWord' : 'modals.model.triggerWords.copyOrEditWord');
|
||||
|
||||
const escapedWord = escapeHtml(word);
|
||||
tag.innerHTML = `
|
||||
@@ -537,7 +537,7 @@ function setupDisplayTriggerWordTag(tag) {
|
||||
|
||||
tag.addEventListener('click', handleDisplayTriggerWordClick);
|
||||
tag.addEventListener('dblclick', handleDisplayTriggerWordDoubleClick);
|
||||
tag.title = translate('modals.model.triggerWords.copyWord');
|
||||
tag.title = translate('modals.model.triggerWords.copyOrEditWord');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
||||
import { onboardingManager } from './OnboardingManager.js';
|
||||
|
||||
/**
|
||||
* Manages help modal functionality and tutorial update notifications
|
||||
*/
|
||||
export class HelpManager {
|
||||
constructor() {
|
||||
this.lastViewedTimestamp = getStorageItem('help_last_viewed', 0);
|
||||
this.latestContentTimestamp = new Date('2025-10-11').getTime(); // Will be updated from server or config
|
||||
// Version of the help content the user has seen. Compared against the
|
||||
// data-help-content-version marker rendered into the help modal markup,
|
||||
// so badge state is always derived from the content actually served.
|
||||
this.viewedContentVersion = getStorageItem('help_viewed_content_version', null);
|
||||
this.isInitialized = false;
|
||||
|
||||
// Default latest content data - could be fetched from server
|
||||
this.latestVideoData = {
|
||||
timestamp: new Date('2024-06-09').getTime(), // Default timestamp
|
||||
walkthrough: {
|
||||
id: 'hvKw31YpE-U',
|
||||
title: 'Getting Started with LoRA Manager'
|
||||
},
|
||||
playlistUpdated: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,9 +27,6 @@ export class HelpManager {
|
||||
// Check if we need to show the badge
|
||||
this.updateHelpBadge();
|
||||
|
||||
// Fetch latest video data (could be implemented to fetch from remote source)
|
||||
this.fetchLatestVideoData();
|
||||
|
||||
this.isInitialized = true;
|
||||
return this;
|
||||
}
|
||||
@@ -55,77 +45,147 @@ export class HelpManager {
|
||||
const tabButtons = document.querySelectorAll('.help-tabs .tab-btn');
|
||||
tabButtons.forEach(button => {
|
||||
button.addEventListener('click', (event) => {
|
||||
// Remove active class from all buttons and panes
|
||||
document.querySelectorAll('.help-tabs .tab-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.help-content .tab-pane').forEach(pane => {
|
||||
pane.classList.remove('active');
|
||||
});
|
||||
|
||||
// Add active class to clicked button
|
||||
event.currentTarget.classList.add('active');
|
||||
|
||||
// Show corresponding tab content
|
||||
const tabId = event.currentTarget.getAttribute('data-tab');
|
||||
document.getElementById(tabId).classList.add('active');
|
||||
this.activateHelpTab(event.currentTarget.getAttribute('data-tab'));
|
||||
});
|
||||
});
|
||||
|
||||
// Replay tutorial button in the Getting Started tab
|
||||
const replayTutorialBtn = document.getElementById('replayTutorialBtn');
|
||||
if (replayTutorialBtn) {
|
||||
replayTutorialBtn.addEventListener('click', () => {
|
||||
// Close the help modal, then restart the onboarding tutorial
|
||||
if (window.modalManager) {
|
||||
window.modalManager.closeModal('helpModal');
|
||||
}
|
||||
onboardingManager.reset();
|
||||
onboardingManager.startTutorial();
|
||||
});
|
||||
}
|
||||
|
||||
// Global "?" shortcut opens the help modal on the Shortcuts tab
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key !== '?') return;
|
||||
if (this.isTypingContext(event.target)) return;
|
||||
if (window.modalManager?.isAnyModalOpen()) return;
|
||||
|
||||
event.preventDefault();
|
||||
this.openHelpModal('shortcuts');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the event target is a text entry context where "?" is literal input
|
||||
*/
|
||||
isTypingContext(target) {
|
||||
if (!(target instanceof Element)) return false;
|
||||
|
||||
const tagName = target.tagName?.toLowerCase();
|
||||
return target.isContentEditable || tagName === 'input' || tagName === 'textarea' || tagName === 'select';
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a specific help modal tab by its data-tab id
|
||||
* @param {string} tabId - The tab id (matches data-tab and pane element id)
|
||||
*/
|
||||
activateHelpTab(tabId) {
|
||||
const tabButton = document.querySelector(`.help-tabs .tab-btn[data-tab="${tabId}"]`);
|
||||
const tabPane = document.getElementById(tabId);
|
||||
if (!tabButton || !tabPane) return;
|
||||
|
||||
// Remove active class from all buttons and panes
|
||||
document.querySelectorAll('.help-tabs .tab-btn').forEach(btn => {
|
||||
btn.classList.remove('active');
|
||||
});
|
||||
document.querySelectorAll('.help-content .tab-pane').forEach(pane => {
|
||||
pane.classList.remove('active');
|
||||
});
|
||||
|
||||
// Activate the requested tab
|
||||
tabButton.classList.add('active');
|
||||
tabPane.classList.add('active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the help modal
|
||||
* @param {string} [tabId] - Optional tab id to activate after opening
|
||||
*/
|
||||
openHelpModal() {
|
||||
openHelpModal(tabId) {
|
||||
// Use modalManager to open the help modal
|
||||
if (window.modalManager) {
|
||||
window.modalManager.toggleModal('helpModal');
|
||||
|
||||
// Add visual indicator to Documentation tab if there's new content
|
||||
this.updateDocumentationTabIndicator();
|
||||
|
||||
// Update the last viewed timestamp
|
||||
this.markContentAsViewed();
|
||||
|
||||
// Hide the badge
|
||||
this.hideHelpBadge();
|
||||
if (!window.modalManager) return;
|
||||
|
||||
const hadNewContent = this.hasNewContent();
|
||||
|
||||
window.modalManager.toggleModal('helpModal');
|
||||
|
||||
if (tabId) {
|
||||
this.activateHelpTab(tabId);
|
||||
}
|
||||
|
||||
// Only acknowledge the content as viewed when the user opened the
|
||||
// modal while it actually contained new content. Opening a stale
|
||||
// (pre-upgrade) page must not suppress the badge after a refresh.
|
||||
if (hadNewContent) {
|
||||
this.updateNewContentTabIndicators();
|
||||
this.markContentAsViewed();
|
||||
}
|
||||
|
||||
// Hide the badge
|
||||
this.hideHelpBadge();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add visual indicator to Documentation tab for new content
|
||||
* Add visual indicator to tabs that received new content
|
||||
*/
|
||||
updateDocumentationTabIndicator() {
|
||||
const docTab = document.querySelector('.tab-btn[data-tab="documentation"]');
|
||||
if (docTab && this.hasNewContent()) {
|
||||
docTab.classList.add('has-new-content');
|
||||
updateNewContentTabIndicators() {
|
||||
if (!this.hasNewContent()) return;
|
||||
|
||||
// Tabs updated in the 2026-09-03 discoverability release:
|
||||
// getting-started (Replay Tutorial button) and shortcuts (new cheat-sheet tab)
|
||||
const NEW_CONTENT_TABS = ['getting-started', 'shortcuts'];
|
||||
NEW_CONTENT_TABS.forEach(tabId => {
|
||||
const tab = document.querySelector(`.help-tabs .tab-btn[data-tab="${tabId}"]`);
|
||||
if (tab) {
|
||||
tab.classList.add('has-new-content');
|
||||
}
|
||||
});
|
||||
|
||||
// Point the indicator at the specific new element inside the
|
||||
// Getting Started tab, and scroll it into view so it is not lost
|
||||
// below the fold of the modal body.
|
||||
const replayBtn = document.getElementById('replayTutorialBtn');
|
||||
if (replayBtn) {
|
||||
replayBtn.classList.add('has-new-content');
|
||||
const gettingStartedActive = document.querySelector('#getting-started.tab-pane.active');
|
||||
if (gettingStartedActive && typeof replayBtn.scrollIntoView === 'function') {
|
||||
replayBtn.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark content as viewed by saving current timestamp
|
||||
* Mark content as viewed by persisting the version rendered in the DOM.
|
||||
* No-op when the served markup carries no version marker (stale assets),
|
||||
* so viewing old content never suppresses the badge for new content.
|
||||
*/
|
||||
markContentAsViewed() {
|
||||
this.lastViewedTimestamp = Date.now();
|
||||
setStorageItem('help_last_viewed', this.lastViewedTimestamp);
|
||||
const currentVersion = this.getCurrentContentVersion();
|
||||
if (!currentVersion) return;
|
||||
|
||||
this.viewedContentVersion = currentVersion;
|
||||
setStorageItem('help_viewed_content_version', this.viewedContentVersion);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetch latest video data (could be implemented to actually fetch from a remote source)
|
||||
* Read the help content version from the rendered modal markup
|
||||
* @returns {string|null} Version marker, or null if the served markup has none
|
||||
*/
|
||||
fetchLatestVideoData() {
|
||||
// In a real implementation, you'd fetch this from your server
|
||||
// For now, we'll just use the hardcoded data from constructor
|
||||
|
||||
// Update the timestamp with the latest data
|
||||
this.latestContentTimestamp = Math.max(this.latestContentTimestamp, this.latestVideoData.timestamp);
|
||||
|
||||
// Check again if we need to show the badge with this new data
|
||||
this.updateHelpBadge();
|
||||
getCurrentContentVersion() {
|
||||
const marker = document.querySelector('[data-help-content-version]');
|
||||
return marker ? marker.getAttribute('data-help-content-version') : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update help badge visibility based on timestamps
|
||||
* Update help badge visibility based on viewed vs. served content version
|
||||
*/
|
||||
updateHelpBadge() {
|
||||
if (this.hasNewContent()) {
|
||||
@@ -134,13 +194,13 @@ export class HelpManager {
|
||||
this.hideHelpBadge();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if there's new content the user hasn't seen
|
||||
* Check if the served help content is newer than what the user has viewed
|
||||
*/
|
||||
hasNewContent() {
|
||||
// If user has never viewed the help, or the content is newer than last viewed
|
||||
return this.lastViewedTimestamp === 0 || this.latestContentTimestamp > this.lastViewedTimestamp;
|
||||
const currentVersion = this.getCurrentContentVersion();
|
||||
return Boolean(currentVersion) && currentVersion !== this.viewedContentVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,7 +43,7 @@ export class OnboardingManager {
|
||||
{
|
||||
target: '.controls .action-buttons [data-action="bulk"]',
|
||||
title: () => translate('onboarding.steps.bulk.title', {}, 'Bulk Operations'),
|
||||
content: () => translate('onboarding.steps.bulk.content', {}, 'Enter bulk mode by clicking this button or pressing <span class="onboarding-shortcut">B</span>. Select multiple models and perform batch operations. Use <span class="onboarding-shortcut">Ctrl+A</span> to select all visible models.'),
|
||||
content: () => translate('onboarding.steps.bulk.content', {}, 'Enter bulk mode by clicking this button or pressing <span class="onboarding-shortcut">B</span> to select multiple models and perform batch operations.<br>• <span class="onboarding-shortcut">Ctrl/Cmd+A</span> select all visible models, <span class="onboarding-shortcut">Shift+Click</span> select a range.<br>• <span class="onboarding-shortcut">Esc</span> or clicking an empty area exits bulk mode.'),
|
||||
position: 'bottom'
|
||||
},
|
||||
{
|
||||
@@ -71,10 +71,30 @@ export class OnboardingManager {
|
||||
position: 'top',
|
||||
customPosition: { top: '20%', left: '50%' }
|
||||
},
|
||||
{
|
||||
target: '.card-grid',
|
||||
title: () => translate('onboarding.steps.marqueeSelect.title', {}, 'Drag to Select'),
|
||||
content: () => translate('onboarding.steps.marqueeSelect.content', {}, 'Hold the <strong>left mouse button</strong> on an empty area of the grid and drag to draw a marquee that selects multiple cards at once.'),
|
||||
position: 'top',
|
||||
customPosition: { top: '20%', left: '50%' }
|
||||
},
|
||||
{
|
||||
target: '#folderSidebar',
|
||||
title: () => translate('onboarding.steps.dragToSidebar.title', {}, 'Organize by Dragging'),
|
||||
content: () => translate('onboarding.steps.dragToSidebar.content', {}, 'Drag a model card onto a folder in the sidebar to move the file there. This also works with multiple selected cards in bulk mode.'),
|
||||
position: 'right'
|
||||
},
|
||||
{
|
||||
target: '.card-grid',
|
||||
title: () => translate('onboarding.steps.contextMenu.title', {}, 'Context Menu'),
|
||||
content: () => translate('onboarding.steps.contextMenu.content', {}, '<strong>Right-click</strong> any model card for a context menu with additional actions.'),
|
||||
content: () => translate('onboarding.steps.contextMenu.content', {}, '<strong>Right-click</strong> any model card for a context menu with card actions like moving, deleting, or editing metadata.'),
|
||||
position: 'top',
|
||||
customPosition: { top: '20%', left: '50%' }
|
||||
},
|
||||
{
|
||||
target: '.card-grid',
|
||||
title: () => translate('onboarding.steps.contextMenus.title', {}, 'More Context Menus'),
|
||||
content: () => translate('onboarding.steps.contextMenus.content', {}, 'In bulk mode, <strong>right-click a selected card</strong> for bulk actions. <strong>Right-click an empty area</strong> of the page for global actions like update checks and managing excluded models.'),
|
||||
position: 'top',
|
||||
customPosition: { top: '20%', left: '50%' }
|
||||
}
|
||||
|
||||
@@ -311,6 +311,20 @@ export function showActionToast(key, params = {}, type = 'info', options = {}) {
|
||||
toast.append(closeBtn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the event target is a text-entry context (input, textarea,
|
||||
* select, or contenteditable) where single-letter shortcuts should be treated
|
||||
* as literal input.
|
||||
* @param {EventTarget|null} target - The DOM event target
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isTypingContext(target) {
|
||||
if (!(target instanceof Element)) return false;
|
||||
|
||||
const tagName = target.tagName?.toLowerCase();
|
||||
return target.isContentEditable || tagName === 'input' || tagName === 'textarea' || tagName === 'select';
|
||||
}
|
||||
|
||||
export function restoreFolderFilter() {
|
||||
const activeFolder = getStorageItem('activeFolder');
|
||||
const folderTag = activeFolder && document.querySelector(`.tag[data-folder="${activeFolder}"]`);
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
</select>
|
||||
</div>
|
||||
<div title="{% if page_id == 'recipes' %}{{ t('recipes.controls.refresh.title') }}{% else %}{{ t('loras.controls.refresh.title') }}{% endif %}" class="control-group dropdown-group">
|
||||
<button data-action="refresh" class="dropdown-main"><i class="fas fa-sync"></i> <span>{{ t('common.actions.refresh') }}</span></button>
|
||||
<button data-action="refresh" class="dropdown-main"><i class="fas fa-sync"></i> <span><span>{{ t('common.actions.refresh') }}</span> <kbd class="shortcut-key">R</kbd></span></button>
|
||||
<button class="dropdown-toggle" aria-label="Show refresh options">
|
||||
<i class="fas fa-caret-down"></i>
|
||||
</button>
|
||||
@@ -78,11 +78,11 @@
|
||||
|
||||
{% if page_id != 'recipes' %}
|
||||
<div class="control-group">
|
||||
<button data-action="fetch" title="{{ t('loras.controls.fetch.title') }}"><i class="fas fa-download"></i> <span>{{ t('loras.controls.fetch.action') }}</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 class="control-group">
|
||||
<button data-action="download" title="{{ t('loras.controls.download.title') }}">
|
||||
<i class="fas fa-cloud-download-alt"></i> <span>{{ t('loras.controls.download.action') }}</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>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -96,7 +96,7 @@
|
||||
{% endif %}
|
||||
<div class="control-group">
|
||||
<button id="bulkOperationsBtn" data-action="bulk" title="{{ t('loras.controls.bulk.title') }}">
|
||||
<i class="fas fa-th-large"></i> <span><span>{{ t('loras.controls.bulk.action') }}</span> <div class="shortcut-key">B</div></span>
|
||||
<i class="fas fa-th-large"></i> <span><span>{{ t('loras.controls.bulk.action') }}</span> <kbd class="shortcut-key">B</kbd></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="control-group">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- Help Modal -->
|
||||
<div id="helpModal" class="modal">
|
||||
<div id="helpModal" class="modal" data-help-content-version="2026-09-03">
|
||||
<div class="modal-content help-modal">
|
||||
<button class="close" onclick="modalManager.closeModal('helpModal')">×</button>
|
||||
<div class="help-header">
|
||||
@@ -10,6 +10,7 @@
|
||||
<button class="tab-btn active" data-tab="getting-started">{{ t('help.tabs.gettingStarted') }}</button>
|
||||
<button class="tab-btn" data-tab="update-vlogs">{{ t('help.tabs.updateVlogs') }}</button>
|
||||
<button class="tab-btn" data-tab="documentation">{{ t('help.tabs.documentation') }}</button>
|
||||
<button class="tab-btn" data-tab="shortcuts">{{ t('help.tabs.shortcuts') }}</button>
|
||||
</div>
|
||||
|
||||
<div class="help-content">
|
||||
@@ -39,6 +40,13 @@
|
||||
<li><strong>Recipe System:</strong> Create, save and share your perfect combinations</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="help-actions">
|
||||
<button id="replayTutorialBtn" class="replay-tutorial-btn">
|
||||
<i class="fas fa-graduation-cap"></i>
|
||||
<span>{{ t('help.gettingStarted.replayTutorial') }}</span>
|
||||
<span class="new-content-badge">{{ t('help.newContentBadge') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Update Vlogs Tab -->
|
||||
@@ -136,6 +144,126 @@
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Shortcuts Tab -->
|
||||
<div class="tab-pane" id="shortcuts">
|
||||
<h3>{{ t('help.shortcuts.title') }}</h3>
|
||||
|
||||
<div class="shortcuts-section">
|
||||
<h4><i class="fas fa-keyboard"></i> {{ t('help.shortcuts.groups.general') }}</h4>
|
||||
<ul class="shortcuts-list">
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>Ctrl</kbd><span class="shortcut-sep">/</span><kbd>Cmd</kbd><span class="shortcut-sep">+</span><kbd>F</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.focusSearch') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>Esc</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.closeModal') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>?</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.openShortcuts') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts-section">
|
||||
<h4><i class="fas fa-bolt"></i> {{ t('help.shortcuts.groups.actions') }}</h4>
|
||||
<ul class="shortcuts-list">
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>R</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.refresh') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>F</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.fetchMetadata') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>D</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.downloadModel') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts-section">
|
||||
<h4><i class="fas fa-object-group"></i> {{ t('help.shortcuts.groups.selection') }}</h4>
|
||||
<ul class="shortcuts-list">
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>B</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.toggleBulkMode') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>Ctrl</kbd><span class="shortcut-sep">/</span><kbd>Cmd</kbd><span class="shortcut-sep">+</span><kbd>A</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.selectAll') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>Shift</kbd><span class="shortcut-sep">+</span><kbd>{{ t('help.shortcuts.keys.click') }}</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.rangeSelect') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>{{ t('help.shortcuts.keys.drag') }}</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.marqueeSelect') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>Esc</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.exitBulkMode') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>{{ t('help.shortcuts.keys.rightClick') }}</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.bulkActions') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>{{ t('help.shortcuts.keys.rightClick') }}</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.globalActions') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts-section">
|
||||
<h4><i class="fas fa-arrows-alt-v"></i> {{ t('help.shortcuts.groups.navigation') }}</h4>
|
||||
<ul class="shortcuts-list">
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>PageUp</kbd><span class="shortcut-sep">/</span><kbd>PageDown</kbd><span class="shortcut-sep">/</span><kbd>Home</kbd><span class="shortcut-sep">/</span><kbd>End</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.scrollPages') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>Alt</kbd><span class="shortcut-sep">+</span><kbd>{{ t('help.shortcuts.keys.letter') }}</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.jumpAlphabet') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts-section">
|
||||
<h4><i class="fas fa-window-restore"></i> {{ t('help.shortcuts.groups.modelModal') }}</h4>
|
||||
<ul class="shortcuts-list">
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>←</kbd><span class="shortcut-sep">/</span><kbd>→</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.prevNext') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>Delete</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.deleteEntry') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="shortcuts-section">
|
||||
<h4><i class="fas fa-images"></i> {{ t('help.shortcuts.groups.mediaViewer') }}</h4>
|
||||
<ul class="shortcuts-list">
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>←</kbd><span class="shortcut-sep">/</span><kbd>→</kbd><span class="shortcut-sep">/</span><kbd>[</kbd><span class="shortcut-sep">/</span><kbd>]</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.cycleMedia') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>{{ t('help.shortcuts.keys.swipe') }}</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.swipeTouch') }}</span>
|
||||
</li>
|
||||
<li>
|
||||
<span class="shortcut-keys"><kbd>Esc</kbd></span>
|
||||
<span class="shortcut-description">{{ t('help.shortcuts.entries.closeViewer') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -133,4 +133,124 @@ describe('AutoComplete active-filters flag', () => {
|
||||
expect(call[0]).not.toContain('base_model=');
|
||||
}
|
||||
});
|
||||
|
||||
const typeLorasSlashCommand = async () => {
|
||||
const input = document.createElement('textarea');
|
||||
input.value = '/';
|
||||
input.selectionStart = 1;
|
||||
document.body.append(input);
|
||||
|
||||
caretHelperInstance.getBeforeCursor.mockReturnValue('/');
|
||||
|
||||
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
|
||||
const autoComplete = new AutoComplete(input, 'loras', { showPreview: false, minChars: 1 });
|
||||
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
return autoComplete;
|
||||
};
|
||||
|
||||
it('shows the active-filters state below the loras slash command list', async () => {
|
||||
await typeLorasSlashCommand();
|
||||
|
||||
const footer = document.querySelector('.lm-autocomplete-command-footer');
|
||||
expect(footer).not.toBeNull();
|
||||
expect(footer.textContent).toContain('Active Filters Search: OFF');
|
||||
expect(footer.textContent).toContain('/activefilters to enable');
|
||||
});
|
||||
|
||||
it('shows how to disable active-filters search in the footer when it is on', async () => {
|
||||
settingGetMock.mockImplementation((key) => {
|
||||
if (key === 'loramanager.lora_active_filters_autocomplete') {
|
||||
return true;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await typeLorasSlashCommand();
|
||||
|
||||
const footer = document.querySelector('.lm-autocomplete-command-footer');
|
||||
expect(footer).not.toBeNull();
|
||||
expect(footer.textContent).toContain('Active Filters Search: ON');
|
||||
expect(footer.textContent).toContain('/noactivefilters to disable');
|
||||
});
|
||||
|
||||
it('shows a dismissible first-run hint on loras suggestions and remembers dismissal', async () => {
|
||||
fetchApiMock.mockResolvedValue({
|
||||
json: () => Promise.resolve({
|
||||
success: true,
|
||||
relative_paths: ['models/example.safetensors'],
|
||||
}),
|
||||
});
|
||||
|
||||
const triggerSearch = async () => {
|
||||
const input = document.createElement('textarea');
|
||||
input.value = 'example';
|
||||
input.selectionStart = 7;
|
||||
document.body.append(input);
|
||||
|
||||
caretHelperInstance.getBeforeCursor.mockReturnValue('example');
|
||||
|
||||
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
|
||||
const autoComplete = new AutoComplete(input, 'loras', {
|
||||
debounceDelay: 0,
|
||||
showPreview: false,
|
||||
minChars: 1,
|
||||
});
|
||||
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
await Promise.resolve();
|
||||
return autoComplete;
|
||||
};
|
||||
|
||||
const autoComplete = await triggerSearch();
|
||||
const hint = autoComplete.dropdown.querySelector('.lm-autocomplete-first-run-hint');
|
||||
expect(hint).not.toBeNull();
|
||||
expect(hint.textContent).toContain('/activefilters');
|
||||
|
||||
hint.querySelector('button').click();
|
||||
expect(autoComplete.dropdown.querySelector('.lm-autocomplete-first-run-hint')).toBeNull();
|
||||
expect(localStorage.getItem('lm:activefilters-tip-dismissed')).toBe('1');
|
||||
// A fresh instance no longer shows the hint once dismissed
|
||||
const autoComplete2 = await triggerSearch();
|
||||
expect(autoComplete2.dropdown.querySelector('.lm-autocomplete-first-run-hint')).toBeNull();
|
||||
});
|
||||
|
||||
it('does not show the loras first-run hint when active-filters search is already on', async () => {
|
||||
settingGetMock.mockImplementation((key) => {
|
||||
if (key === 'loramanager.lora_active_filters_autocomplete') {
|
||||
return true;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
fetchApiMock.mockResolvedValue({
|
||||
json: () => Promise.resolve({
|
||||
success: true,
|
||||
relative_paths: ['models/example.safetensors'],
|
||||
}),
|
||||
});
|
||||
|
||||
const input = document.createElement('textarea');
|
||||
input.value = 'example';
|
||||
input.selectionStart = 7;
|
||||
document.body.append(input);
|
||||
|
||||
caretHelperInstance.getBeforeCursor.mockReturnValue('example');
|
||||
|
||||
const { AutoComplete } = await import(AUTOCOMPLETE_MODULE);
|
||||
const autoComplete = new AutoComplete(input, 'loras', {
|
||||
debounceDelay: 0,
|
||||
showPreview: false,
|
||||
minChars: 1,
|
||||
});
|
||||
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
await Promise.resolve();
|
||||
|
||||
expect(autoComplete.dropdown.querySelector('.lm-autocomplete-first-run-hint')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
|
||||
const {
|
||||
APP_MODULE,
|
||||
API_MODULE,
|
||||
UTILS_MODULE,
|
||||
SETTINGS_MODULE,
|
||||
LORA_LOADER_MODULE,
|
||||
} = vi.hoisted(() => ({
|
||||
APP_MODULE: new URL("../../../scripts/app.js", import.meta.url).pathname,
|
||||
API_MODULE: new URL("../../../scripts/api.js", import.meta.url).pathname,
|
||||
UTILS_MODULE: new URL("../../../web/comfyui/utils.js", import.meta.url).pathname,
|
||||
SETTINGS_MODULE: new URL("../../../web/comfyui/settings.js", import.meta.url).pathname,
|
||||
LORA_LOADER_MODULE: new URL("../../../web/comfyui/lora_loader.js", import.meta.url).pathname,
|
||||
}));
|
||||
|
||||
const extensionState = { current: null };
|
||||
const registerExtensionMock = vi.fn((extension) => {
|
||||
extensionState.current = extension;
|
||||
});
|
||||
|
||||
vi.mock(APP_MODULE, () => ({
|
||||
app: {
|
||||
registerExtension: registerExtensionMock,
|
||||
graph: {},
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock(API_MODULE, () => ({
|
||||
api: {
|
||||
addEventListener: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const showToastMock = vi.fn();
|
||||
|
||||
vi.mock(UTILS_MODULE, () => ({
|
||||
collectActiveLorasFromChain: vi.fn(),
|
||||
updateConnectedTriggerWords: vi.fn(),
|
||||
mergeLoras: vi.fn(),
|
||||
chainCallback: (proto, property, callback) => {
|
||||
proto[property] = callback;
|
||||
},
|
||||
getAllGraphNodes: vi.fn(),
|
||||
getNodeFromGraph: vi.fn(),
|
||||
getWidgetByName: vi.fn(),
|
||||
getWidgetSerializedValue: vi.fn(),
|
||||
showToast: showToastMock,
|
||||
}));
|
||||
|
||||
const getActiveFiltersPreferenceMock = vi.fn();
|
||||
const setSettingValueMock = vi.fn();
|
||||
|
||||
vi.mock(SETTINGS_MODULE, () => ({
|
||||
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID:
|
||||
"loramanager.lora_active_filters_autocomplete",
|
||||
getLoraActiveFiltersAutocompletePreference: getActiveFiltersPreferenceMock,
|
||||
setLoraManagerSettingValue: setSettingValueMock,
|
||||
}));
|
||||
|
||||
async function registerNodeType(comfyClass) {
|
||||
await import(LORA_LOADER_MODULE);
|
||||
const extension = extensionState.current;
|
||||
expect(extension).toBeDefined();
|
||||
const nodeType = { comfyClass, prototype: {} };
|
||||
await extension.beforeRegisterNodeDef(nodeType, {}, {});
|
||||
return nodeType;
|
||||
}
|
||||
|
||||
function getMenuOption(nodeType, enabled) {
|
||||
getActiveFiltersPreferenceMock.mockReturnValue(enabled);
|
||||
const options = [];
|
||||
nodeType.prototype.getExtraMenuOptions(null, options);
|
||||
return options.find(
|
||||
(option) =>
|
||||
option &&
|
||||
typeof option.content === "string" &&
|
||||
option.content.startsWith("Active Filters Search:")
|
||||
);
|
||||
}
|
||||
|
||||
describe("Lora Loader active-filters context menu", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
extensionState.current = null;
|
||||
registerExtensionMock.mockClear();
|
||||
showToastMock.mockClear();
|
||||
getActiveFiltersPreferenceMock.mockReset();
|
||||
setSettingValueMock.mockReset();
|
||||
setSettingValueMock.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"Lora Loader (LoraManager)",
|
||||
"Lora Stacker (LoraManager)",
|
||||
"WanVideo Lora Select (LoraManager)",
|
||||
"Create Hook LoRA (LoraManager)",
|
||||
])("adds the toggle entry to the %s context menu", async (comfyClass) => {
|
||||
const nodeType = await registerNodeType(comfyClass);
|
||||
|
||||
const option = getMenuOption(nodeType, false);
|
||||
expect(option).toBeDefined();
|
||||
expect(option.content).toContain("Active Filters Search: OFF");
|
||||
expect(option.content).toContain("/activefilters to enable");
|
||||
});
|
||||
|
||||
it("shows the disable hint when active-filters search is on", async () => {
|
||||
const nodeType = await registerNodeType("Lora Loader (LoraManager)");
|
||||
|
||||
const option = getMenuOption(nodeType, true);
|
||||
expect(option.content).toContain("Active Filters Search: ON");
|
||||
expect(option.content).toContain("/noactivefilters to disable");
|
||||
});
|
||||
|
||||
it("toggles the setting and toasts feedback", async () => {
|
||||
const nodeType = await registerNodeType("Lora Loader (LoraManager)");
|
||||
|
||||
const enableOption = getMenuOption(nodeType, false);
|
||||
await enableOption.callback();
|
||||
|
||||
expect(setSettingValueMock).toHaveBeenCalledWith(
|
||||
"loramanager.lora_active_filters_autocomplete",
|
||||
true
|
||||
);
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ summary: "Active Filters Search Enabled" })
|
||||
);
|
||||
|
||||
const disableOption = getMenuOption(nodeType, true);
|
||||
await disableOption.callback();
|
||||
|
||||
expect(setSettingValueMock).toHaveBeenCalledWith(
|
||||
"loramanager.lora_active_filters_autocomplete",
|
||||
false
|
||||
);
|
||||
expect(showToastMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ summary: "Active Filters Search Disabled" })
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -43,6 +43,12 @@ vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
|
||||
showToast: showToastMock,
|
||||
openCivitaiByMetadata: openCivitaiByMetadataMock,
|
||||
updatePanelPositions: updatePanelPositionsMock,
|
||||
// Faithful stand-in for the real helper in uiHelpers.js
|
||||
isTypingContext: (target) => {
|
||||
if (!(target instanceof Element)) return false;
|
||||
const tagName = target.tagName?.toLowerCase();
|
||||
return target.isContentEditable || tagName === 'input' || tagName === 'textarea' || tagName === 'select';
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/managers/DownloadManager.js', () => ({
|
||||
@@ -1200,4 +1206,87 @@ describe('PageControls favorites, sorting, and duplicates scenarios', () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PageControls action keyboard shortcuts', () => {
|
||||
async function setupLorasControls() {
|
||||
renderControlsDom('loras');
|
||||
const stateModule = await import('../../../static/js/state/index.js');
|
||||
stateModule.initPageState('loras');
|
||||
const { LorasControls } = await import('../../../static/js/components/controls/LorasControls.js');
|
||||
return new LorasControls();
|
||||
}
|
||||
|
||||
function keydownEvent(key, { target = document.body, ...init } = {}) {
|
||||
const event = new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true, ...init });
|
||||
Object.defineProperty(event, 'target', { value: target });
|
||||
return event;
|
||||
}
|
||||
|
||||
it('registers a pageControls-actions keydown handler with the event manager', async () => {
|
||||
await setupLorasControls();
|
||||
|
||||
const { eventManager } = await import('../../../static/js/utils/EventManager.js');
|
||||
const keydownHandlers = eventManager.handlers.get('keydown') || [];
|
||||
expect(keydownHandlers.some((h) => h.source === 'pageControls-actions')).toBe(true);
|
||||
});
|
||||
|
||||
it('triggers refresh, fetch, and download via the R / F / D keys', async () => {
|
||||
const controls = await setupLorasControls();
|
||||
|
||||
expect(controls.handleActionShortcut(keydownEvent('r'))).toBe(true);
|
||||
expect(refreshModelsMock).toHaveBeenCalledWith(false);
|
||||
|
||||
expect(controls.handleActionShortcut(keydownEvent('f'))).toBe(true);
|
||||
expect(fetchCivitaiMetadataMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(controls.handleActionShortcut(keydownEvent('d'))).toBe(true);
|
||||
expect(downloadManagerMock.showDownloadModal).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('handles a real keydown dispatched on the document', async () => {
|
||||
await setupLorasControls();
|
||||
|
||||
const event = new KeyboardEvent('keydown', { key: 'r', bubbles: true, cancelable: true });
|
||||
document.dispatchEvent(event);
|
||||
|
||||
expect(event.defaultPrevented).toBe(true);
|
||||
expect(refreshModelsMock).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it('ignores R / F / D while typing in an input', async () => {
|
||||
const controls = await setupLorasControls();
|
||||
|
||||
const input = document.getElementById('searchInput');
|
||||
for (const key of ['r', 'f', 'd']) {
|
||||
const event = keydownEvent(key, { target: input });
|
||||
expect(controls.handleActionShortcut(event)).toBe(false);
|
||||
expect(event.defaultPrevented).toBe(false);
|
||||
}
|
||||
|
||||
expect(refreshModelsMock).not.toHaveBeenCalled();
|
||||
expect(fetchCivitaiMetadataMock).not.toHaveBeenCalled();
|
||||
expect(downloadManagerMock.showDownloadModal).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ignores R / F / D when combined with modifier keys', async () => {
|
||||
const controls = await setupLorasControls();
|
||||
|
||||
const event = keydownEvent('r', { ctrlKey: true });
|
||||
expect(controls.handleActionShortcut(event)).toBe(false);
|
||||
expect(event.defaultPrevented).toBe(false);
|
||||
expect(refreshModelsMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes the event through when the action button does not exist', async () => {
|
||||
const controls = await setupLorasControls();
|
||||
|
||||
// Recipes page has no fetch/download buttons
|
||||
document.querySelector('[data-action="fetch"]').closest('.control-group').remove();
|
||||
|
||||
const event = keydownEvent('f');
|
||||
expect(controls.handleActionShortcut(event)).toBe(false);
|
||||
expect(event.defaultPrevented).toBe(false);
|
||||
expect(fetchCivitaiMetadataMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,12 @@ vi.mock('../../../static/js/utils/uiHelpers.js', () => ({
|
||||
showToast: vi.fn(),
|
||||
openCivitaiByMetadata: vi.fn(),
|
||||
updatePanelPositions: vi.fn(),
|
||||
// Faithful stand-in for the real helper in uiHelpers.js
|
||||
isTypingContext: (target) => {
|
||||
if (!(target instanceof Element)) return false;
|
||||
const tagName = target.tagName?.toLowerCase();
|
||||
return target.isContentEditable || tagName === 'input' || tagName === 'textarea' || tagName === 'select';
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../../static/js/managers/DownloadManager.js', () => ({
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { describe, it, beforeEach, expect, vi } from 'vitest';
|
||||
|
||||
const HELP_MANAGER_MODULE = new URL('../../../static/js/managers/HelpManager.js', import.meta.url).pathname;
|
||||
const VIEWED_KEY = 'lora_manager_help_viewed_content_version';
|
||||
|
||||
function setupDom({ versionMarker = null } = {}) {
|
||||
const markerAttr = versionMarker ? ` data-help-content-version="${versionMarker}"` : '';
|
||||
document.body.innerHTML = `
|
||||
<div class="help-toggle" id="helpToggleBtn">
|
||||
<span class="update-badge"></span>
|
||||
</div>
|
||||
<div id="helpModal" class="modal"${markerAttr}>
|
||||
<div class="help-tabs">
|
||||
<button class="tab-btn" data-tab="getting-started"></button>
|
||||
<button class="tab-btn" data-tab="shortcuts"></button>
|
||||
</div>
|
||||
<div class="tab-pane active" id="getting-started">
|
||||
<button id="replayTutorialBtn" class="replay-tutorial-btn"></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
describe('HelpManager content-version badge logic', () => {
|
||||
let HelpManager;
|
||||
|
||||
beforeEach(async () => {
|
||||
({ HelpManager } = await import(HELP_MANAGER_MODULE));
|
||||
});
|
||||
|
||||
function badgeIsVisible() {
|
||||
return document.querySelector('#helpToggleBtn .update-badge').classList.contains('visible');
|
||||
}
|
||||
|
||||
it('has no new content when the served markup carries no version marker', () => {
|
||||
setupDom({ versionMarker: null });
|
||||
const manager = new HelpManager();
|
||||
|
||||
expect(manager.hasNewContent()).toBe(false);
|
||||
manager.updateHelpBadge();
|
||||
expect(badgeIsVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('has new content when a version marker exists and nothing has been viewed yet', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
const manager = new HelpManager();
|
||||
|
||||
expect(manager.hasNewContent()).toBe(true);
|
||||
manager.updateHelpBadge();
|
||||
expect(badgeIsVisible()).toBe(true);
|
||||
});
|
||||
|
||||
it('has no new content once the stored viewed version matches the marker', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
localStorage.setItem(VIEWED_KEY, '2026-09-03');
|
||||
const manager = new HelpManager();
|
||||
|
||||
expect(manager.hasNewContent()).toBe(false);
|
||||
manager.updateHelpBadge();
|
||||
expect(badgeIsVisible()).toBe(false);
|
||||
});
|
||||
|
||||
it('has new content again when the marker moves to a newer version', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
localStorage.setItem(VIEWED_KEY, '2025-10-11');
|
||||
const manager = new HelpManager();
|
||||
|
||||
expect(manager.hasNewContent()).toBe(true);
|
||||
});
|
||||
|
||||
it('markContentAsViewed persists the DOM marker version', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
const manager = new HelpManager();
|
||||
|
||||
manager.markContentAsViewed();
|
||||
|
||||
expect(localStorage.getItem(VIEWED_KEY)).toBe('2026-09-03');
|
||||
expect(manager.hasNewContent()).toBe(false);
|
||||
});
|
||||
|
||||
it('markContentAsViewed is a no-op without a version marker (stale assets)', () => {
|
||||
setupDom({ versionMarker: null });
|
||||
const manager = new HelpManager();
|
||||
|
||||
manager.markContentAsViewed();
|
||||
|
||||
expect(localStorage.getItem(VIEWED_KEY)).toBeNull();
|
||||
});
|
||||
|
||||
it('opening the help modal without new content does not mark it as viewed', () => {
|
||||
// Regression test: on a stale (pre-upgrade) page the user may open the
|
||||
// help modal before refreshing; that must not suppress the badge for
|
||||
// the new content they have not seen yet.
|
||||
setupDom({ versionMarker: null });
|
||||
window.modalManager = { toggleModal: vi.fn() };
|
||||
const manager = new HelpManager();
|
||||
|
||||
manager.openHelpModal();
|
||||
|
||||
expect(localStorage.getItem(VIEWED_KEY)).toBeNull();
|
||||
expect(manager.hasNewContent()).toBe(false);
|
||||
delete window.modalManager;
|
||||
});
|
||||
|
||||
it('opening the help modal with new content marks it as viewed and hides the badge', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
window.modalManager = { toggleModal: vi.fn() };
|
||||
const manager = new HelpManager();
|
||||
manager.updateHelpBadge();
|
||||
expect(badgeIsVisible()).toBe(true);
|
||||
|
||||
manager.openHelpModal();
|
||||
|
||||
expect(localStorage.getItem(VIEWED_KEY)).toBe('2026-09-03');
|
||||
expect(badgeIsVisible()).toBe(false);
|
||||
delete window.modalManager;
|
||||
});
|
||||
|
||||
it('adds new-content indicators to the getting-started and shortcuts tabs', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
const manager = new HelpManager();
|
||||
|
||||
manager.updateNewContentTabIndicators();
|
||||
|
||||
expect(document.querySelector('.help-tabs .tab-btn[data-tab="getting-started"]').classList.contains('has-new-content')).toBe(true);
|
||||
expect(document.querySelector('.help-tabs .tab-btn[data-tab="shortcuts"]').classList.contains('has-new-content')).toBe(true);
|
||||
});
|
||||
|
||||
it('flags the Replay Tutorial button and scrolls it into view', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
const replayBtn = document.getElementById('replayTutorialBtn');
|
||||
replayBtn.scrollIntoView = vi.fn();
|
||||
const manager = new HelpManager();
|
||||
|
||||
manager.updateNewContentTabIndicators();
|
||||
|
||||
expect(replayBtn.classList.contains('has-new-content')).toBe(true);
|
||||
expect(replayBtn.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth', block: 'nearest' });
|
||||
});
|
||||
|
||||
it('does not flag the Replay Tutorial button when the content is not new', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
localStorage.setItem(VIEWED_KEY, '2026-09-03');
|
||||
const manager = new HelpManager();
|
||||
|
||||
manager.updateNewContentTabIndicators();
|
||||
|
||||
expect(document.getElementById('replayTutorialBtn').classList.contains('has-new-content')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not scroll the Replay Tutorial button when the getting-started tab is inactive', () => {
|
||||
setupDom({ versionMarker: '2026-09-03' });
|
||||
document.getElementById('getting-started').classList.remove('active');
|
||||
const replayBtn = document.getElementById('replayTutorialBtn');
|
||||
replayBtn.scrollIntoView = vi.fn();
|
||||
const manager = new HelpManager();
|
||||
|
||||
manager.updateNewContentTabIndicators();
|
||||
|
||||
expect(replayBtn.scrollIntoView).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<div class="autocomplete-text-widget">
|
||||
<div class="input-wrapper">
|
||||
<div
|
||||
ref="inputWrapperRef"
|
||||
class="input-wrapper"
|
||||
:style="{ '--lm-vscrollbar-width': vScrollbarWidth + 'px' }"
|
||||
>
|
||||
<textarea
|
||||
ref="textareaRef"
|
||||
:placeholder="placeholder"
|
||||
@@ -59,9 +63,49 @@ const isVueDomMode = ref(typeof LiteGraph !== 'undefined' && LiteGraph.vueNodesM
|
||||
const onModeChange = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ isVueDomMode: boolean }>
|
||||
isVueDomMode.value = customEvent.detail.isVueDomMode
|
||||
// Canvas vs Vue DOM mode use different paddings/geometry.
|
||||
updateVScrollbarWidth()
|
||||
}
|
||||
|
||||
const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||
const inputWrapperRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// Width of the textarea's own vertical scrollbar gutter. When the content
|
||||
// overflows and a classic (non-overlay) scrollbar is shown, the scrollbar
|
||||
// occupies the textarea's rightmost pixels and the absolutely-positioned
|
||||
// corner clear (x) button would overlap it. We expose this width as a CSS
|
||||
// var so the button can shift left of the scrollbar; it is 0 when there is
|
||||
// no scrollbar (content fits, or platform overlay scrollbars that float
|
||||
// over the content).
|
||||
const vScrollbarWidth = ref(0)
|
||||
let scrollbarResizeObserver: ResizeObserver | null = null
|
||||
|
||||
const updateVScrollbarWidth = () => {
|
||||
const ta = textareaRef.value
|
||||
if (!ta) return
|
||||
const overflowsY = ta.scrollHeight > ta.clientHeight
|
||||
vScrollbarWidth.value = overflowsY ? ta.offsetWidth - ta.clientWidth : 0
|
||||
}
|
||||
|
||||
const observeScrollbarWidth = () => {
|
||||
unobserveScrollbarWidth()
|
||||
const ta = textareaRef.value
|
||||
if (!ta || typeof ResizeObserver === 'undefined') {
|
||||
return
|
||||
}
|
||||
scrollbarResizeObserver = new ResizeObserver(() => {
|
||||
updateVScrollbarWidth()
|
||||
})
|
||||
scrollbarResizeObserver.observe(ta)
|
||||
}
|
||||
|
||||
const unobserveScrollbarWidth = () => {
|
||||
if (scrollbarResizeObserver) {
|
||||
scrollbarResizeObserver.disconnect()
|
||||
scrollbarResizeObserver = null
|
||||
}
|
||||
}
|
||||
|
||||
const hasText = ref(false)
|
||||
|
||||
// Show clear button when there is text
|
||||
@@ -79,6 +123,9 @@ const updateHasTextState = () => {
|
||||
}
|
||||
|
||||
const onInput = (event: Event) => {
|
||||
// Content may grow/shrink past the overflow threshold on each edit.
|
||||
updateVScrollbarWidth()
|
||||
|
||||
// A clear via execCommand captures the full-text selection in the browser's
|
||||
// undo entry; Ctrl+Z restores the content together with that selection.
|
||||
// Collapse the caret so the restored text is not left selected.
|
||||
@@ -153,6 +200,9 @@ const onWheel = (event: WheelEvent) => {
|
||||
|
||||
// Handle external value changes (e.g., from "send lora to workflow")
|
||||
const onExternalValueChange = () => {
|
||||
// The DOM value was set synchronously by the caller; the new content may
|
||||
// have toggled the vertical scrollbar on/off.
|
||||
updateVScrollbarWidth()
|
||||
updateHasTextState()
|
||||
}
|
||||
|
||||
@@ -162,6 +212,9 @@ const setupWidgetOnSetValue = () => {
|
||||
props.widget.onSetValue = (value: string) => {
|
||||
// The DOM value is already set by setValue, just update our state
|
||||
hasText.value = value.length > 0
|
||||
// Programmatic sets can toggle the scrollbar; re-measure so the
|
||||
// corner clear button stays clear of it.
|
||||
updateVScrollbarWidth()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -252,11 +305,17 @@ onMounted(() => {
|
||||
// Setup widget.onSetValue callback
|
||||
setupWidgetOnSetValue()
|
||||
|
||||
// Keep the corner buttons clear of the textarea's vertical scrollbar.
|
||||
updateVScrollbarWidth()
|
||||
observeScrollbarWidth()
|
||||
|
||||
// Listen for custom event dispatched by main.ts
|
||||
document.addEventListener('lora-manager:vue-mode-change', onModeChange)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unobserveScrollbarWidth()
|
||||
|
||||
// Clean up textarea reference
|
||||
if (props.widget.inputEl === textareaRef.value) {
|
||||
props.widget.inputEl = undefined
|
||||
@@ -333,7 +392,7 @@ onUnmounted(() => {
|
||||
/* Clear button styles */
|
||||
.clear-button {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
right: calc(6px + var(--lm-vscrollbar-width, 0px));
|
||||
bottom: 6px; /* Changed from top to bottom */
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
@@ -371,7 +430,7 @@ onUnmounted(() => {
|
||||
|
||||
/* Vue DOM mode adjustments for clear button */
|
||||
.text-input.vue-dom-mode ~ .clear-button {
|
||||
right: 8px;
|
||||
right: calc(8px + var(--lm-vscrollbar-width, 0px));
|
||||
bottom: 10px; /* Changed from top to bottom, adjusted for Vue DOM padding */
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
|
||||
@@ -16,7 +16,7 @@ import AutocompleteTextWidget from '@/components/AutocompleteTextWidget.vue'
|
||||
function createMockWidget() {
|
||||
return {
|
||||
callback: vi.fn(),
|
||||
onSetValue: undefined,
|
||||
onSetValue: undefined as ((v: string) => void) | undefined,
|
||||
inputEl: undefined,
|
||||
metadataWidget: undefined,
|
||||
name: 'text',
|
||||
@@ -134,3 +134,148 @@ describe('AutocompleteTextWidget clear button', () => {
|
||||
expect(widget.callback).toHaveBeenLastCalledWith('hello world')
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Tests for the vertical-scrollbar inset.
|
||||
*
|
||||
* When the textarea content overflows and a classic (non-overlay) scrollbar
|
||||
* is shown, the absolutely-positioned corner clear (x) button would sit on
|
||||
* top of the scrollbar. The component measures the scrollbar gutter and
|
||||
* exposes it as the --lm-vscrollbar-width CSS var on .input-wrapper so the
|
||||
* button shifts left of the scrollbar. jsdom does no layout, so overflow is
|
||||
* simulated by overriding the scroll/dimension props.
|
||||
*/
|
||||
describe('AutocompleteTextWidget vertical scrollbar inset', () => {
|
||||
function overrideTextareaMetrics(
|
||||
ta: HTMLTextAreaElement,
|
||||
metrics: { scrollHeight: number; clientHeight: number; offsetWidth: number; clientWidth: number }
|
||||
) {
|
||||
Object.defineProperty(ta, 'scrollHeight', { configurable: true, value: metrics.scrollHeight })
|
||||
Object.defineProperty(ta, 'clientHeight', { configurable: true, value: metrics.clientHeight })
|
||||
Object.defineProperty(ta, 'offsetWidth', { configurable: true, value: metrics.offsetWidth })
|
||||
Object.defineProperty(ta, 'clientWidth', { configurable: true, value: metrics.clientWidth })
|
||||
}
|
||||
|
||||
it('exposes the scrollbar width as a CSS var when the content overflows', async () => {
|
||||
const { wrapper } = mountWidget()
|
||||
const textarea = wrapper.find('textarea').element as HTMLTextAreaElement
|
||||
|
||||
overrideTextareaMetrics(textarea, {
|
||||
scrollHeight: 200,
|
||||
clientHeight: 100,
|
||||
offsetWidth: 320,
|
||||
clientWidth: 305, // 15px scrollbar gutter
|
||||
})
|
||||
textarea.dispatchEvent(new Event('input'))
|
||||
await nextTick()
|
||||
|
||||
const wrapperEl = wrapper.find('.input-wrapper').element as HTMLElement
|
||||
expect(wrapperEl.style.getPropertyValue('--lm-vscrollbar-width')).toBe('15px')
|
||||
})
|
||||
|
||||
it('keeps the CSS var at 0px when there is no vertical scrollbar', async () => {
|
||||
const { wrapper } = mountWidget()
|
||||
const textarea = wrapper.find('textarea').element as HTMLTextAreaElement
|
||||
|
||||
overrideTextareaMetrics(textarea, {
|
||||
scrollHeight: 100,
|
||||
clientHeight: 100,
|
||||
offsetWidth: 320,
|
||||
clientWidth: 320,
|
||||
})
|
||||
textarea.dispatchEvent(new Event('input'))
|
||||
await nextTick()
|
||||
|
||||
const wrapperEl = wrapper.find('.input-wrapper').element as HTMLElement
|
||||
expect(wrapperEl.style.getPropertyValue('--lm-vscrollbar-width')).toBe('0px')
|
||||
})
|
||||
|
||||
it('clears the inset once overflowing content is removed', async () => {
|
||||
const { wrapper } = mountWidget()
|
||||
const textarea = wrapper.find('textarea').element as HTMLTextAreaElement
|
||||
|
||||
overrideTextareaMetrics(textarea, {
|
||||
scrollHeight: 200,
|
||||
clientHeight: 100,
|
||||
offsetWidth: 320,
|
||||
clientWidth: 305,
|
||||
})
|
||||
textarea.dispatchEvent(new Event('input'))
|
||||
await nextTick()
|
||||
const wrapperEl = wrapper.find('.input-wrapper').element as HTMLElement
|
||||
expect(wrapperEl.style.getPropertyValue('--lm-vscrollbar-width')).toBe('15px')
|
||||
|
||||
// Content now fits: no scrollbar → inset cleared
|
||||
overrideTextareaMetrics(textarea, {
|
||||
scrollHeight: 100,
|
||||
clientHeight: 100,
|
||||
offsetWidth: 320,
|
||||
clientWidth: 320,
|
||||
})
|
||||
textarea.dispatchEvent(new Event('input'))
|
||||
await nextTick()
|
||||
expect(wrapperEl.style.getPropertyValue('--lm-vscrollbar-width')).toBe('0px')
|
||||
})
|
||||
|
||||
it('re-measures the inset when the value is set programmatically via onSetValue', async () => {
|
||||
const { wrapper, widget } = mountWidget()
|
||||
const textarea = wrapper.find('textarea').element as HTMLTextAreaElement
|
||||
|
||||
// Start with no overflow
|
||||
overrideTextareaMetrics(textarea, {
|
||||
scrollHeight: 100,
|
||||
clientHeight: 100,
|
||||
offsetWidth: 320,
|
||||
clientWidth: 320,
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
// Simulate an external setValue (e.g. "send lora to workflow"): the DOM
|
||||
// value is set by the caller and widget.onSetValue fires without an
|
||||
// input event. Content now overflows → inset must be re-measured.
|
||||
overrideTextareaMetrics(textarea, {
|
||||
scrollHeight: 200,
|
||||
clientHeight: 100,
|
||||
offsetWidth: 320,
|
||||
clientWidth: 305, // 15px scrollbar gutter
|
||||
})
|
||||
if (!widget.onSetValue) throw new Error('onSetValue not installed by component')
|
||||
widget.onSetValue('long content')
|
||||
await nextTick()
|
||||
|
||||
const wrapperEl = wrapper.find('.input-wrapper').element as HTMLElement
|
||||
expect(wrapperEl.style.getPropertyValue('--lm-vscrollbar-width')).toBe('15px')
|
||||
})
|
||||
|
||||
it('re-measures the inset on external value-change events', async () => {
|
||||
const { wrapper } = mountWidget()
|
||||
const textarea = wrapper.find('textarea').element as HTMLTextAreaElement
|
||||
|
||||
// Start with no overflow
|
||||
overrideTextareaMetrics(textarea, {
|
||||
scrollHeight: 100,
|
||||
clientHeight: 100,
|
||||
offsetWidth: 320,
|
||||
clientWidth: 320,
|
||||
})
|
||||
await nextTick()
|
||||
|
||||
// The lora-manager:autocomplete-value-changed event fires when the
|
||||
// widget value is set externally; content now overflows → re-measure.
|
||||
overrideTextareaMetrics(textarea, {
|
||||
scrollHeight: 200,
|
||||
clientHeight: 100,
|
||||
offsetWidth: 320,
|
||||
clientWidth: 305, // 15px scrollbar gutter
|
||||
})
|
||||
textarea.dispatchEvent(
|
||||
new CustomEvent('lora-manager:autocomplete-value-changed', {
|
||||
detail: { value: 'long content' },
|
||||
})
|
||||
)
|
||||
await nextTick()
|
||||
|
||||
const wrapperEl = wrapper.find('.input-wrapper').element as HTMLElement
|
||||
expect(wrapperEl.style.getPropertyValue('--lm-vscrollbar-width')).toBe('15px')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,86 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
|
||||
import { resolve } from 'path'
|
||||
import { dirname, resolve } from 'path'
|
||||
|
||||
// Specifiers that must stay external. The bundle is emitted to
|
||||
// web/comfyui/vue-widgets/, and ComfyUI serves that directory's parent
|
||||
// (web/comfyui) at /extensions/ComfyUI-Lora-Manager/, so one "../" from the
|
||||
// bundle reaches web/comfyui modules and three "../../.." reach ComfyUI's
|
||||
// own runtime scripts at runtime.
|
||||
//
|
||||
// scripts/app.js and scripts/api.js are intentionally NOT listed here: they
|
||||
// are externalized by the keep-runtime-modules-external plugin below, which
|
||||
// also rewrites the shallower "../../scripts/*" specifiers used by modules
|
||||
// inlined from web/comfyui/ so every binding dedupes into a single import.
|
||||
const EXTERNAL_SPECIFIERS = [
|
||||
'../loras_widget.js',
|
||||
'../autocomplete.js',
|
||||
'../preview_tooltip.js'
|
||||
]
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
cssInjectedByJsPlugin() // Inject CSS into JS for ComfyUI compatibility
|
||||
cssInjectedByJsPlugin(), // Inject CSS into JS for ComfyUI compatibility
|
||||
// Keep shared runtime modules external instead of inlining them into
|
||||
// the bundle. This guards against the inlined-shim bug class (the
|
||||
// removed active-filters chip ended up writing settings to a dead
|
||||
// in-memory store this way):
|
||||
//
|
||||
// 1. Modules under web/comfyui/ import the repo-root scripts/app.js
|
||||
// TEST SHIM as "../../scripts/app.js" — a depth that resolves to
|
||||
// the shim on the build filesystem but 404s at the bundle's
|
||||
// runtime location. Rewrite to the canonical bundle-depth
|
||||
// specifier so every app/api binding in the bundle is the REAL
|
||||
// ComfyUI module.
|
||||
// 2. web/comfyui/settings.js must never be duplicated into the
|
||||
// bundle: it registers settings via a module-level side effect
|
||||
// and owns module state. Externalize it to "../settings.js" so
|
||||
// the bundle binds to the SAME vanilla module instance that the
|
||||
// ComfyUI extension loader already loaded.
|
||||
{
|
||||
name: 'lora-manager:keep-runtime-modules-external',
|
||||
enforce: 'pre',
|
||||
resolveId(source, importer) {
|
||||
const scriptsMatch = source.match(/^(\.\.\/)+scripts\/(app|api)\.js$/)
|
||||
if (scriptsMatch) {
|
||||
return { id: `../../../scripts/${scriptsMatch[2]}.js`, external: true }
|
||||
}
|
||||
if (
|
||||
importer &&
|
||||
/[\\/]web[\\/]comfyui[\\/]settings\.js$/.test(
|
||||
resolve(dirname(importer), source)
|
||||
)
|
||||
) {
|
||||
return { id: '../settings.js', external: true }
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
// Warning twin of the rewrite above: importing web/comfyui/* from
|
||||
// widget source inlines that module into the bundle, duplicating any
|
||||
// module-level side effects/state it owns. The settings.js and
|
||||
// scripts/app|api.js imports are made safe by the plugin above, but
|
||||
// review any further such import deliberately.
|
||||
{
|
||||
name: 'lora-manager:warn-web-comfyui-imports',
|
||||
enforce: 'pre',
|
||||
resolveId(source, importer) {
|
||||
if (
|
||||
importer &&
|
||||
/[\\/]vue-widgets[\\/]src[\\/]/.test(importer) &&
|
||||
/[\\/]web[\\/]comfyui[\\/]/.test(source)
|
||||
) {
|
||||
this.warn(
|
||||
`[vue-widgets] Inlining web/comfyui module "${source}" into the bundle. ` +
|
||||
'settings.js and scripts/app|api.js imports are externalized by this config, ' +
|
||||
'but the inlined copy still duplicates module-level side effects — verify that is intended.'
|
||||
)
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -20,13 +94,7 @@ export default defineConfig({
|
||||
fileName: 'lora-manager-widgets'
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
'../../../scripts/app.js',
|
||||
'../../../scripts/api.js',
|
||||
'../loras_widget.js',
|
||||
'../autocomplete.js',
|
||||
'../preview_tooltip.js'
|
||||
],
|
||||
external: EXTERNAL_SPECIFIERS,
|
||||
output: {
|
||||
dir: '../web/comfyui/vue-widgets',
|
||||
entryFileNames: 'lora-manager-widgets.js',
|
||||
@@ -41,3 +109,4 @@ export default defineConfig({
|
||||
'process.env.NODE_ENV': JSON.stringify('production')
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
+48
-16
@@ -23,6 +23,8 @@ import { showToast } from "./utils.js";
|
||||
|
||||
// localStorage key for the one-time "how to disable" hint in the dropdown
|
||||
const FIRST_RUN_HINT_DISMISSED_KEY = 'lm:autocomplete-disable-tip-dismissed';
|
||||
// localStorage key for the one-time "try active filters search" hint (loras nodes)
|
||||
const ACTIVE_FILTERS_HINT_DISMISSED_KEY = 'lm:activefilters-tip-dismissed';
|
||||
|
||||
// Command definitions for category filtering
|
||||
const TAG_COMMANDS = {
|
||||
@@ -1763,22 +1765,31 @@ class AutoComplete {
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a state hint below the slash command list so the autocomplete
|
||||
* toggle commands explain themselves. Only applies to prompt nodes.
|
||||
* Render a state hint below the slash command list so the toggle commands
|
||||
* explain themselves. Prompt nodes advertise /autocomplete, loras nodes
|
||||
* advertise /activefilters.
|
||||
*/
|
||||
_renderCommandListFooter() {
|
||||
this._removeCommandListFooter();
|
||||
|
||||
if (this.modelType !== 'prompt') {
|
||||
let text = null;
|
||||
if (this.modelType === 'prompt') {
|
||||
const enabled = getPromptTagAutocompletePreference();
|
||||
text = enabled
|
||||
? 'Tag autocomplete is ON — /noautocomplete to disable'
|
||||
: 'Tag autocomplete is OFF — /autocomplete to enable';
|
||||
} else if (this.modelType === 'loras') {
|
||||
const enabled = getLoraActiveFiltersAutocompletePreference();
|
||||
text = enabled
|
||||
? 'Active Filters Search: ON — /noactivefilters to disable'
|
||||
: 'Active Filters Search: OFF — /activefilters to enable';
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const enabled = getPromptTagAutocompletePreference();
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'lm-autocomplete-command-footer';
|
||||
footer.textContent = enabled
|
||||
? 'Tag autocomplete is ON — /noautocomplete to disable'
|
||||
: 'Tag autocomplete is OFF — /autocomplete to enable';
|
||||
footer.textContent = text;
|
||||
footer.style.cssText = `
|
||||
padding: 6px 12px;
|
||||
font-size: 11px;
|
||||
@@ -1801,23 +1812,44 @@ class AutoComplete {
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a one-time, dismissible hint inside the dropdown telling users how
|
||||
* to disable tag autocomplete. Dismissal is persisted in localStorage.
|
||||
* Show a one-time, dismissible hint inside the dropdown surfacing the
|
||||
* toggle commands: prompt nodes advertise /noautocomplete, loras nodes
|
||||
* advertise /activefilters. Dismissal is persisted in localStorage.
|
||||
*/
|
||||
_maybeShowFirstRunHint() {
|
||||
if (this.firstRunHint) {
|
||||
return;
|
||||
}
|
||||
if (this.modelType !== 'prompt'
|
||||
|| this.showingCommands
|
||||
|| this.searchType !== 'custom_words'
|
||||
|| this.activeCommand) {
|
||||
|
||||
let hintText = null;
|
||||
let storageKey = null;
|
||||
|
||||
if (this.modelType === 'prompt') {
|
||||
// Only hint during plain tag searches, not command/embedding modes
|
||||
if (this.showingCommands
|
||||
|| this.searchType !== 'custom_words'
|
||||
|| this.activeCommand) {
|
||||
return;
|
||||
}
|
||||
hintText = 'Tip: type /noautocomplete to turn off these suggestions';
|
||||
storageKey = FIRST_RUN_HINT_DISMISSED_KEY;
|
||||
} else if (this.modelType === 'loras') {
|
||||
// Only advertise active-filters search while it is disabled
|
||||
if (this.showingCommands || this.activeCommand) {
|
||||
return;
|
||||
}
|
||||
if (getLoraActiveFiltersAutocompletePreference()) {
|
||||
return;
|
||||
}
|
||||
hintText = 'Tip: type /activefilters to search within the LoRA Manager page filters';
|
||||
storageKey = ACTIVE_FILTERS_HINT_DISMISSED_KEY;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
let dismissed = false;
|
||||
try {
|
||||
dismissed = localStorage.getItem(FIRST_RUN_HINT_DISMISSED_KEY) === '1';
|
||||
dismissed = localStorage.getItem(storageKey) === '1';
|
||||
} catch (e) {
|
||||
// localStorage unavailable - fall through and show the hint
|
||||
}
|
||||
@@ -1839,7 +1871,7 @@ class AutoComplete {
|
||||
`;
|
||||
|
||||
const text = document.createElement('span');
|
||||
text.textContent = 'Tip: type /noautocomplete to turn off these suggestions';
|
||||
text.textContent = hintText;
|
||||
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.type = 'button';
|
||||
@@ -1856,7 +1888,7 @@ class AutoComplete {
|
||||
`;
|
||||
closeBtn.addEventListener('click', () => {
|
||||
try {
|
||||
localStorage.setItem(FIRST_RUN_HINT_DISMISSED_KEY, '1');
|
||||
localStorage.setItem(storageKey, '1');
|
||||
} catch (e) {
|
||||
}
|
||||
this._removeFirstRunHint();
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
import {
|
||||
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID,
|
||||
getLoraActiveFiltersAutocompletePreference,
|
||||
setLoraManagerSettingValue,
|
||||
} from "./settings.js";
|
||||
import { showToast } from "./utils.js";
|
||||
import {
|
||||
collectActiveLorasFromChain,
|
||||
updateConnectedTriggerWords,
|
||||
@@ -12,6 +18,65 @@ import {
|
||||
} from "./utils.js";
|
||||
import { applyLoraValuesToText, debounce } from "./lora_syntax_utils.js";
|
||||
|
||||
// Node classes whose "text" widget uses the loras autocomplete. Kept in sync
|
||||
// with the broadcast-compatible classes in handleLoraCodeUpdate below.
|
||||
const LORA_AUTOCOMPLETE_NODE_CLASSES = [
|
||||
"Lora Loader (LoraManager)",
|
||||
"Lora Stacker (LoraManager)",
|
||||
"WanVideo Lora Select (LoraManager)",
|
||||
"Create Hook LoRA (LoraManager)",
|
||||
];
|
||||
|
||||
// Expose the active-filters search toggle in the node's right-click menu so
|
||||
// users can discover the switch where the behavior actually happens, instead
|
||||
// of only via the /activefilters and /noactivefilters slash commands. Mirrors
|
||||
// the tag-autocomplete menu entry on Prompt (LoraManager) nodes.
|
||||
function addActiveFiltersSearchMenuOption(nodeType) {
|
||||
const getExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
|
||||
nodeType.prototype.getExtraMenuOptions = function (_, options) {
|
||||
getExtraMenuOptions?.apply?.(this, arguments);
|
||||
|
||||
options.push(null);
|
||||
|
||||
const filtersSearchEnabled = getLoraActiveFiltersAutocompletePreference();
|
||||
options.push({
|
||||
content: filtersSearchEnabled
|
||||
? "Active Filters Search: ON (/noactivefilters to disable)"
|
||||
: "Active Filters Search: OFF (/activefilters to enable)",
|
||||
callback: async () => {
|
||||
const newValue = !filtersSearchEnabled;
|
||||
try {
|
||||
const success = await setLoraManagerSettingValue(
|
||||
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID,
|
||||
newValue
|
||||
);
|
||||
if (!success) {
|
||||
throw new Error("settings API unavailable");
|
||||
}
|
||||
showToast({
|
||||
severity: newValue ? "success" : "secondary",
|
||||
summary: newValue
|
||||
? "Active Filters Search Enabled"
|
||||
: "Active Filters Search Disabled",
|
||||
detail: newValue
|
||||
? "LoRA autocomplete now respects the active filters of the LoRA Manager page. Type /noactivefilters in the LoRA field to disable."
|
||||
: "LoRA autocomplete searches the full library again. Type /activefilters in the LoRA field to re-enable.",
|
||||
life: 3000,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("[Lora Manager] Failed to toggle setting:", error);
|
||||
showToast({
|
||||
severity: "error",
|
||||
summary: "Error",
|
||||
detail: "Failed to toggle active filters search setting",
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "LoraManager.LoraLoader",
|
||||
|
||||
@@ -35,12 +100,7 @@ app.registerExtension({
|
||||
// Handle broadcast mode (for Desktop/non-browser support)
|
||||
if (numericNodeId === -1) {
|
||||
// Find all compatible nodes in the current graph
|
||||
const compatibleClasses = new Set([
|
||||
"Lora Loader (LoraManager)",
|
||||
"Lora Stacker (LoraManager)",
|
||||
"WanVideo Lora Select (LoraManager)",
|
||||
"Create Hook LoRA (LoraManager)",
|
||||
]);
|
||||
const compatibleClasses = new Set(LORA_AUTOCOMPLETE_NODE_CLASSES);
|
||||
const targetNodes = getAllGraphNodes(app.graph)
|
||||
.map(({ node }) => node)
|
||||
.filter((node) => compatibleClasses.has(node?.comfyClass));
|
||||
@@ -108,6 +168,10 @@ app.registerExtension({
|
||||
},
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (LORA_AUTOCOMPLETE_NODE_CLASSES.includes(nodeType.comfyClass)) {
|
||||
addActiveFiltersSearchMenuOption(nodeType);
|
||||
}
|
||||
|
||||
if (nodeType.comfyClass == "Lora Loader (LoraManager)") {
|
||||
chainCallback(nodeType.prototype, "onNodeCreated", function () {
|
||||
// Enable widget serialization
|
||||
|
||||
@@ -450,7 +450,7 @@ app.registerExtension({
|
||||
name: "Search LoRA autocomplete within active filters",
|
||||
type: "boolean",
|
||||
defaultValue: LORA_ACTIVE_FILTERS_AUTOCOMPLETE_DEFAULT,
|
||||
tooltip: "When enabled, LoRA autocomplete suggestions respect the active filters (folder/base model/tags) set in the LoRA Manager page. Commands /activefilters and /noactivefilters toggle this mode.",
|
||||
tooltip: "When enabled, LoRA autocomplete suggestions respect the active filters (folder/base model/tags) set in the LoRA Manager page. You can also toggle it by typing /activefilters or /noactivefilters in the LoRA field, or from the node's right-click menu.",
|
||||
category: ["LoRA Manager", "Autocomplete", "LoRA Active Filters"],
|
||||
},
|
||||
{
|
||||
@@ -597,6 +597,7 @@ app.registerExtension({
|
||||
|
||||
export {
|
||||
PROMPT_TAG_AUTOCOMPLETE_SETTING_ID,
|
||||
LORA_ACTIVE_FILTERS_AUTOCOMPLETE_SETTING_ID,
|
||||
getWheelSensitivity,
|
||||
getAutoPathCorrectionPreference,
|
||||
getAutocompleteAppendCommaPreference,
|
||||
|
||||
@@ -2118,14 +2118,14 @@ to { transform: rotate(360deg);
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.autocomplete-text-widget[data-v-4e322fec] {
|
||||
.autocomplete-text-widget[data-v-793d67d2] {
|
||||
background: transparent;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.input-wrapper[data-v-4e322fec] {
|
||||
.input-wrapper[data-v-793d67d2] {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
@@ -2133,7 +2133,7 @@ to { transform: rotate(360deg);
|
||||
}
|
||||
|
||||
/* Canvas mode styles (default) - matches built-in comfy-multiline-input */
|
||||
.text-input[data-v-4e322fec] {
|
||||
.text-input[data-v-793d67d2] {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
background-color: var(--comfy-input-bg, #222);
|
||||
@@ -2152,7 +2152,7 @@ to { transform: rotate(360deg);
|
||||
}
|
||||
|
||||
/* Vue DOM mode styles - matches built-in p-textarea in Vue DOM mode */
|
||||
.text-input.vue-dom-mode[data-v-4e322fec] {
|
||||
.text-input.vue-dom-mode[data-v-793d67d2] {
|
||||
background-color: var(--color-charcoal-400, #313235);
|
||||
color: #fff;
|
||||
padding: 8px 12px 30px 12px; /* Reserve bottom space for clear button */
|
||||
@@ -2161,14 +2161,14 @@ to { transform: rotate(360deg);
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.text-input[data-v-4e322fec]:focus {
|
||||
.text-input[data-v-793d67d2]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Clear button styles */
|
||||
.clear-button[data-v-4e322fec] {
|
||||
.clear-button[data-v-793d67d2] {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
right: calc(6px + var(--lm-vscrollbar-width, 0px));
|
||||
bottom: 6px; /* Changed from top to bottom */
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
@@ -2189,31 +2189,31 @@ to { transform: rotate(360deg);
|
||||
}
|
||||
|
||||
/* Show clear button when hovering over input wrapper */
|
||||
.input-wrapper:hover .clear-button[data-v-4e322fec] {
|
||||
.input-wrapper:hover .clear-button[data-v-793d67d2] {
|
||||
opacity: 0.7;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.clear-button[data-v-4e322fec]:hover {
|
||||
.clear-button[data-v-793d67d2]:hover {
|
||||
opacity: 1;
|
||||
background: rgba(255, 100, 100, 0.8);
|
||||
}
|
||||
.clear-button svg[data-v-4e322fec] {
|
||||
.clear-button svg[data-v-793d67d2] {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
/* Vue DOM mode adjustments for clear button */
|
||||
.text-input.vue-dom-mode ~ .clear-button[data-v-4e322fec] {
|
||||
right: 8px;
|
||||
.text-input.vue-dom-mode ~ .clear-button[data-v-793d67d2] {
|
||||
right: calc(8px + var(--lm-vscrollbar-width, 0px));
|
||||
bottom: 10px; /* Changed from top to bottom, adjusted for Vue DOM padding */
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: rgba(107, 114, 128, 0.6);
|
||||
}
|
||||
.text-input.vue-dom-mode ~ .clear-button[data-v-4e322fec]:hover {
|
||||
.text-input.vue-dom-mode ~ .clear-button[data-v-793d67d2]:hover {
|
||||
background: oklch(62% 0.18 25);
|
||||
}
|
||||
.text-input.vue-dom-mode ~ .clear-button svg[data-v-4e322fec] {
|
||||
.text-input.vue-dom-mode ~ .clear-button svg[data-v-793d67d2] {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
@@ -2481,8 +2481,9 @@ to { transform: rotate(360deg);
|
||||
}
|
||||
})();
|
||||
var _a;
|
||||
import { app as app$1 } from "../../../scripts/app.js";
|
||||
import { api as api$1 } from "../../../scripts/api.js";
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
import "../settings.js";
|
||||
/**
|
||||
* @vue/shared v3.5.26
|
||||
* (c) 2018-present Yuxi (Evan) You and Vue contributors
|
||||
@@ -11052,7 +11053,7 @@ const _sfc_main$o = /* @__PURE__ */ defineComponent({
|
||||
const EditButton = /* @__PURE__ */ _export_sfc(_sfc_main$o, [["__scopeId", "data-v-8da8aa4b"]]);
|
||||
const _hoisted_1$k = { class: "section" };
|
||||
const _hoisted_2$j = { class: "section__header" };
|
||||
const _hoisted_3$h = { class: "section__content" };
|
||||
const _hoisted_3$g = { class: "section__content" };
|
||||
const _hoisted_4$f = {
|
||||
key: 0,
|
||||
class: "section__placeholder"
|
||||
@@ -11082,7 +11083,7 @@ const _sfc_main$n = /* @__PURE__ */ defineComponent({
|
||||
onClick: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("edit"))
|
||||
})
|
||||
]),
|
||||
createBaseVNode("div", _hoisted_3$h, [
|
||||
createBaseVNode("div", _hoisted_3$g, [
|
||||
__props.selected.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_4$f, " All models ")) : (openBlock(), createElementBlock("div", _hoisted_5$d, [
|
||||
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.selected, (name) => {
|
||||
return openBlock(), createBlock(FilterChip, {
|
||||
@@ -11101,7 +11102,7 @@ const _sfc_main$n = /* @__PURE__ */ defineComponent({
|
||||
const BaseModelSection = /* @__PURE__ */ _export_sfc(_sfc_main$n, [["__scopeId", "data-v-12f059e2"]]);
|
||||
const _hoisted_1$j = { class: "section" };
|
||||
const _hoisted_2$i = { class: "section__columns" };
|
||||
const _hoisted_3$g = { class: "section__column" };
|
||||
const _hoisted_3$f = { class: "section__column" };
|
||||
const _hoisted_4$e = { class: "section__column-header" };
|
||||
const _hoisted_5$c = { class: "section__column-content" };
|
||||
const _hoisted_6$c = {
|
||||
@@ -11137,7 +11138,7 @@ const _sfc_main$m = /* @__PURE__ */ defineComponent({
|
||||
createBaseVNode("span", { class: "section__title" }, "TAGS")
|
||||
], -1)),
|
||||
createBaseVNode("div", _hoisted_2$i, [
|
||||
createBaseVNode("div", _hoisted_3$g, [
|
||||
createBaseVNode("div", _hoisted_3$f, [
|
||||
createBaseVNode("div", _hoisted_4$e, [
|
||||
_cache[2] || (_cache[2] = createBaseVNode("span", { class: "section__column-title section__column-title--include" }, "INCLUDE", -1)),
|
||||
createVNode(EditButton, {
|
||||
@@ -11183,7 +11184,7 @@ const _sfc_main$m = /* @__PURE__ */ defineComponent({
|
||||
const TagsSection = /* @__PURE__ */ _export_sfc(_sfc_main$m, [["__scopeId", "data-v-b869b780"]]);
|
||||
const _hoisted_1$i = { class: "section" };
|
||||
const _hoisted_2$h = { class: "section__columns" };
|
||||
const _hoisted_3$f = { class: "section__column" };
|
||||
const _hoisted_3$e = { class: "section__column" };
|
||||
const _hoisted_4$d = { class: "section__column-header" };
|
||||
const _hoisted_5$b = { class: "section__content" };
|
||||
const _hoisted_6$b = {
|
||||
@@ -11231,7 +11232,7 @@ const _sfc_main$l = /* @__PURE__ */ defineComponent({
|
||||
createBaseVNode("span", { class: "section__title" }, "FOLDERS")
|
||||
], -1)),
|
||||
createBaseVNode("div", _hoisted_2$h, [
|
||||
createBaseVNode("div", _hoisted_3$f, [
|
||||
createBaseVNode("div", _hoisted_3$e, [
|
||||
createBaseVNode("div", _hoisted_4$d, [
|
||||
_cache[3] || (_cache[3] = createBaseVNode("span", { class: "section__column-title section__column-title--include" }, "INCLUDE", -1)),
|
||||
createBaseVNode("button", {
|
||||
@@ -11299,7 +11300,7 @@ const _sfc_main$l = /* @__PURE__ */ defineComponent({
|
||||
const FoldersSection = /* @__PURE__ */ _export_sfc(_sfc_main$l, [["__scopeId", "data-v-af9caf84"]]);
|
||||
const _hoisted_1$h = { class: "section" };
|
||||
const _hoisted_2$g = { class: "section__header" };
|
||||
const _hoisted_3$e = { class: "section__toggle" };
|
||||
const _hoisted_3$d = { class: "section__toggle" };
|
||||
const _hoisted_4$c = ["checked"];
|
||||
const _hoisted_5$a = { class: "section__columns" };
|
||||
const _hoisted_6$a = { class: "section__column" };
|
||||
@@ -11355,7 +11356,7 @@ const _sfc_main$k = /* @__PURE__ */ defineComponent({
|
||||
return openBlock(), createElementBlock("div", _hoisted_1$h, [
|
||||
createBaseVNode("div", _hoisted_2$g, [
|
||||
_cache[4] || (_cache[4] = createBaseVNode("span", { class: "section__title" }, "NAME PATTERNS", -1)),
|
||||
createBaseVNode("label", _hoisted_3$e, [
|
||||
createBaseVNode("label", _hoisted_3$d, [
|
||||
createBaseVNode("input", {
|
||||
type: "checkbox",
|
||||
checked: __props.useRegex,
|
||||
@@ -11439,7 +11440,7 @@ const _sfc_main$k = /* @__PURE__ */ defineComponent({
|
||||
const NamePatternsSection = /* @__PURE__ */ _export_sfc(_sfc_main$k, [["__scopeId", "data-v-9995b5ed"]]);
|
||||
const _hoisted_1$g = { class: "section" };
|
||||
const _hoisted_2$f = { class: "section__toggles" };
|
||||
const _hoisted_3$d = { class: "toggle-item" };
|
||||
const _hoisted_3$c = { class: "toggle-item" };
|
||||
const _hoisted_4$b = ["aria-checked"];
|
||||
const _hoisted_5$9 = { class: "toggle-item" };
|
||||
const _hoisted_6$9 = ["aria-checked"];
|
||||
@@ -11457,7 +11458,7 @@ const _sfc_main$j = /* @__PURE__ */ defineComponent({
|
||||
createBaseVNode("span", { class: "section__title" }, "LICENSE")
|
||||
], -1)),
|
||||
createBaseVNode("div", _hoisted_2$f, [
|
||||
createBaseVNode("label", _hoisted_3$d, [
|
||||
createBaseVNode("label", _hoisted_3$c, [
|
||||
_cache[3] || (_cache[3] = createBaseVNode("span", {
|
||||
class: "toggle-item__label",
|
||||
title: "Use the model without crediting the creator"
|
||||
@@ -11497,7 +11498,7 @@ const _sfc_main$j = /* @__PURE__ */ defineComponent({
|
||||
const LicenseSection = /* @__PURE__ */ _export_sfc(_sfc_main$j, [["__scopeId", "data-v-07ddd3df"]]);
|
||||
const _hoisted_1$f = { class: "preview" };
|
||||
const _hoisted_2$e = { class: "preview__title" };
|
||||
const _hoisted_3$c = ["disabled"];
|
||||
const _hoisted_3$b = ["disabled"];
|
||||
const _hoisted_4$a = {
|
||||
key: 0,
|
||||
class: "preview__tooltip"
|
||||
@@ -11556,7 +11557,7 @@ const _sfc_main$i = /* @__PURE__ */ defineComponent({
|
||||
d: "M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"
|
||||
})
|
||||
], -1)
|
||||
])], 10, _hoisted_3$c)
|
||||
])], 10, _hoisted_3$b)
|
||||
], 32),
|
||||
createVNode(Transition, { name: "tooltip" }, {
|
||||
default: withCtx(() => [
|
||||
@@ -11668,7 +11669,7 @@ const _sfc_main$h = /* @__PURE__ */ defineComponent({
|
||||
const LoraPoolSummaryView = /* @__PURE__ */ _export_sfc(_sfc_main$h, [["__scopeId", "data-v-83235a00"]]);
|
||||
const _hoisted_1$d = { class: "lora-pool-modal__header" };
|
||||
const _hoisted_2$c = { class: "lora-pool-modal__title-container" };
|
||||
const _hoisted_3$b = { class: "lora-pool-modal__title" };
|
||||
const _hoisted_3$a = { class: "lora-pool-modal__title" };
|
||||
const _hoisted_4$9 = {
|
||||
key: 0,
|
||||
class: "lora-pool-modal__subtitle"
|
||||
@@ -11728,7 +11729,7 @@ const _sfc_main$g = /* @__PURE__ */ defineComponent({
|
||||
}, [
|
||||
createBaseVNode("div", _hoisted_1$d, [
|
||||
createBaseVNode("div", _hoisted_2$c, [
|
||||
createBaseVNode("h3", _hoisted_3$b, toDisplayString(__props.title), 1),
|
||||
createBaseVNode("h3", _hoisted_3$a, toDisplayString(__props.title), 1),
|
||||
__props.subtitle ? (openBlock(), createElementBlock("p", _hoisted_4$9, toDisplayString(__props.subtitle), 1)) : createCommentVNode("", true)
|
||||
]),
|
||||
createBaseVNode("button", {
|
||||
@@ -11756,7 +11757,7 @@ const _sfc_main$g = /* @__PURE__ */ defineComponent({
|
||||
const ModalWrapper = /* @__PURE__ */ _export_sfc(_sfc_main$g, [["__scopeId", "data-v-7b4de03d"]]);
|
||||
const _hoisted_1$c = { class: "search-container" };
|
||||
const _hoisted_2$b = { class: "model-list" };
|
||||
const _hoisted_3$a = ["checked", "onChange"];
|
||||
const _hoisted_3$9 = ["checked", "onChange"];
|
||||
const _hoisted_4$8 = { class: "model-checkbox-visual" };
|
||||
const _hoisted_5$6 = {
|
||||
key: 0,
|
||||
@@ -11866,7 +11867,7 @@ const _sfc_main$f = /* @__PURE__ */ defineComponent({
|
||||
checked: isSelected(model.name),
|
||||
onChange: ($event) => toggleModel(model.name),
|
||||
class: "model-checkbox"
|
||||
}, null, 40, _hoisted_3$a),
|
||||
}, null, 40, _hoisted_3$9),
|
||||
createBaseVNode("span", _hoisted_4$8, [
|
||||
isSelected(model.name) ? (openBlock(), createElementBlock("svg", _hoisted_5$6, [..._cache[4] || (_cache[4] = [
|
||||
createBaseVNode("path", { d: "M13.854 3.646a.5.5 0 0 1 0 .708l-7 7a.5.5 0 0 1-.708 0l-3.5-3.5a.5.5 0 1 1 .708-.708L6.5 10.293l6.646-6.647a.5.5 0 0 1 .708 0z" }, null, -1)
|
||||
@@ -11887,7 +11888,7 @@ const _sfc_main$f = /* @__PURE__ */ defineComponent({
|
||||
const BaseModelModal = /* @__PURE__ */ _export_sfc(_sfc_main$f, [["__scopeId", "data-v-e02ca44a"]]);
|
||||
const _hoisted_1$b = { class: "search-container" };
|
||||
const _hoisted_2$a = ["onClick"];
|
||||
const _hoisted_3$9 = {
|
||||
const _hoisted_3$8 = {
|
||||
key: 0,
|
||||
class: "no-results"
|
||||
};
|
||||
@@ -12031,7 +12032,7 @@ const _sfc_main$e = /* @__PURE__ */ defineComponent({
|
||||
onClick: ($event) => toggleTag(tag.tag)
|
||||
}, toDisplayString(tag.tag), 11, _hoisted_2$a);
|
||||
}), 128)),
|
||||
visibleTags.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_3$9, " No tags found ")) : createCommentVNode("", true),
|
||||
visibleTags.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_3$8, " No tags found ")) : createCommentVNode("", true),
|
||||
hasMoreTags.value ? (openBlock(), createElementBlock("div", _hoisted_4$7, " Scroll to load more... ")) : createCommentVNode("", true)
|
||||
], 544)
|
||||
]),
|
||||
@@ -12046,7 +12047,7 @@ const _hoisted_2$9 = {
|
||||
key: 1,
|
||||
class: "tree-node__toggle-spacer"
|
||||
};
|
||||
const _hoisted_3$8 = { class: "tree-node__checkbox-label" };
|
||||
const _hoisted_3$7 = { class: "tree-node__checkbox-label" };
|
||||
const _hoisted_4$6 = ["checked"];
|
||||
const _hoisted_5$5 = {
|
||||
key: 0,
|
||||
@@ -12107,7 +12108,7 @@ const _sfc_main$d = /* @__PURE__ */ defineComponent({
|
||||
createBaseVNode("path", { d: "M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z" }, null, -1)
|
||||
])], 2))
|
||||
])) : (openBlock(), createElementBlock("span", _hoisted_2$9)),
|
||||
createBaseVNode("label", _hoisted_3$8, [
|
||||
createBaseVNode("label", _hoisted_3$7, [
|
||||
createBaseVNode("input", {
|
||||
type: "checkbox",
|
||||
class: "tree-node__checkbox",
|
||||
@@ -12153,7 +12154,7 @@ const _sfc_main$d = /* @__PURE__ */ defineComponent({
|
||||
const FolderTreeNode = /* @__PURE__ */ _export_sfc(_sfc_main$d, [["__scopeId", "data-v-90187dd4"]]);
|
||||
const _hoisted_1$9 = { class: "search-container" };
|
||||
const _hoisted_2$8 = { class: "folder-tree" };
|
||||
const _hoisted_3$7 = {
|
||||
const _hoisted_3$6 = {
|
||||
key: 1,
|
||||
class: "no-results"
|
||||
};
|
||||
@@ -12252,7 +12253,7 @@ const _sfc_main$c = /* @__PURE__ */ defineComponent({
|
||||
onToggleExpand: toggleExpand,
|
||||
onToggleSelect: toggleSelect
|
||||
}, null, 8, ["node", "selected", "expanded", "variant"]);
|
||||
}), 128)) : (openBlock(), createElementBlock("div", _hoisted_3$7, " No folders found "))
|
||||
}), 128)) : (openBlock(), createElementBlock("div", _hoisted_3$6, " No folders found "))
|
||||
])
|
||||
]),
|
||||
_: 1
|
||||
@@ -12658,7 +12659,7 @@ const _sfc_main$b = /* @__PURE__ */ defineComponent({
|
||||
const LoraPoolWidget = /* @__PURE__ */ _export_sfc(_sfc_main$b, [["__scopeId", "data-v-ed73eab5"]]);
|
||||
const _hoisted_1$8 = { class: "last-used-preview" };
|
||||
const _hoisted_2$7 = { class: "last-used-preview__content" };
|
||||
const _hoisted_3$6 = ["src", "onError"];
|
||||
const _hoisted_3$5 = ["src", "onError"];
|
||||
const _hoisted_4$5 = {
|
||||
key: 1,
|
||||
class: "last-used-preview__thumb last-used-preview__thumb--placeholder"
|
||||
@@ -12710,7 +12711,7 @@ const _sfc_main$a = /* @__PURE__ */ defineComponent({
|
||||
src: previewUrls.value[lora.name],
|
||||
class: "last-used-preview__thumb",
|
||||
onError: ($event) => onImageError(lora.name)
|
||||
}, null, 40, _hoisted_3$6)) : (openBlock(), createElementBlock("div", _hoisted_4$5, [..._cache[0] || (_cache[0] = [
|
||||
}, null, 40, _hoisted_3$5)) : (openBlock(), createElementBlock("div", _hoisted_4$5, [..._cache[0] || (_cache[0] = [
|
||||
createBaseVNode("svg", {
|
||||
viewBox: "0 0 16 16",
|
||||
fill: "currentColor"
|
||||
@@ -13151,7 +13152,7 @@ const _sfc_main$8 = /* @__PURE__ */ defineComponent({
|
||||
const DualRangeSlider = /* @__PURE__ */ _export_sfc(_sfc_main$8, [["__scopeId", "data-v-e0c8dc9f"]]);
|
||||
const _hoisted_1$5 = { class: "randomizer-settings" };
|
||||
const _hoisted_2$5 = { class: "setting-section" };
|
||||
const _hoisted_3$5 = { class: "count-mode-tabs" };
|
||||
const _hoisted_3$4 = { class: "count-mode-tabs" };
|
||||
const _hoisted_4$4 = ["checked"];
|
||||
const _hoisted_5$3 = ["checked"];
|
||||
const _hoisted_6$3 = { class: "slider-container" };
|
||||
@@ -13216,7 +13217,7 @@ const _sfc_main$7 = /* @__PURE__ */ defineComponent({
|
||||
], -1)),
|
||||
createBaseVNode("div", _hoisted_2$5, [
|
||||
_cache[20] || (_cache[20] = createBaseVNode("label", { class: "setting-label" }, "LoRA Count", -1)),
|
||||
createBaseVNode("div", _hoisted_3$5, [
|
||||
createBaseVNode("div", _hoisted_3$4, [
|
||||
createBaseVNode("label", {
|
||||
class: normalizeClass(["count-mode-tab", { active: __props.countMode === "fixed" }])
|
||||
}, [
|
||||
@@ -13847,7 +13848,7 @@ const _sfc_main$6 = /* @__PURE__ */ defineComponent({
|
||||
const LoraRandomizerWidget = /* @__PURE__ */ _export_sfc(_sfc_main$6, [["__scopeId", "data-v-ca6e8cec"]]);
|
||||
const _hoisted_1$4 = { class: "cycler-settings" };
|
||||
const _hoisted_2$4 = { class: "setting-section progress-section" };
|
||||
const _hoisted_3$4 = { class: "progress-label" };
|
||||
const _hoisted_3$3 = { class: "progress-label" };
|
||||
const _hoisted_4$3 = ["title"];
|
||||
const _hoisted_5$2 = { class: "progress-counter" };
|
||||
const _hoisted_6$2 = { class: "progress-index" };
|
||||
@@ -13972,7 +13973,7 @@ const _sfc_main$5 = /* @__PURE__ */ defineComponent({
|
||||
class: normalizeClass(["progress-info", { disabled: __props.isPauseDisabled }]),
|
||||
onClick: handleOpenSelector
|
||||
}, [
|
||||
createBaseVNode("span", _hoisted_3$4, toDisplayString(__props.isWorkflowExecuting ? "Using LoRA:" : "Next LoRA:"), 1),
|
||||
createBaseVNode("span", _hoisted_3$3, toDisplayString(__props.isWorkflowExecuting ? "Using LoRA:" : "Next LoRA:"), 1),
|
||||
createBaseVNode("span", {
|
||||
class: normalizeClass(["progress-name clickable", { disabled: __props.isPauseDisabled, "no-lora": __props.isNoLora }]),
|
||||
title: __props.currentLoraFilename
|
||||
@@ -14172,7 +14173,7 @@ const _sfc_main$5 = /* @__PURE__ */ defineComponent({
|
||||
const LoraCyclerSettingsView = /* @__PURE__ */ _export_sfc(_sfc_main$5, [["__scopeId", "data-v-f0663be4"]]);
|
||||
const _hoisted_1$3 = { class: "search-container" };
|
||||
const _hoisted_2$3 = { class: "lora-list" };
|
||||
const _hoisted_3$3 = ["onMouseenter", "onClick"];
|
||||
const _hoisted_3$2 = ["onMouseenter", "onClick"];
|
||||
const _hoisted_4$2 = { class: "lora-index" };
|
||||
const _hoisted_5$1 = ["title"];
|
||||
const _hoisted_6$1 = {
|
||||
@@ -14353,7 +14354,7 @@ const _sfc_main$4 = /* @__PURE__ */ defineComponent({
|
||||
title: item.lora.file_name
|
||||
}, toDisplayString(item.lora.file_name), 9, _hoisted_5$1),
|
||||
__props.currentIndex === item.index ? (openBlock(), createElementBlock("span", _hoisted_6$1, "Current")) : createCommentVNode("", true)
|
||||
], 42, _hoisted_3$3);
|
||||
], 42, _hoisted_3$2);
|
||||
}), 128)),
|
||||
filteredList.value.length === 0 ? (openBlock(), createElementBlock("div", _hoisted_7$1, " No LoRAs found ")) : createCommentVNode("", true)
|
||||
])
|
||||
@@ -14969,7 +14970,7 @@ const _hoisted_2$2 = {
|
||||
class: "json-content",
|
||||
ref: "contentRef"
|
||||
};
|
||||
const _hoisted_3$2 = ["innerHTML"];
|
||||
const _hoisted_3$1 = ["innerHTML"];
|
||||
const _hoisted_4$1 = {
|
||||
key: 1,
|
||||
class: "placeholder"
|
||||
@@ -15064,7 +15065,7 @@ const _sfc_main$2 = /* @__PURE__ */ defineComponent({
|
||||
hasMetadata.value ? (openBlock(), createElementBlock("pre", {
|
||||
key: 0,
|
||||
innerHTML: highlightedJson.value
|
||||
}, null, 8, _hoisted_3$2)) : (openBlock(), createElementBlock("div", _hoisted_4$1, "No metadata available"))
|
||||
}, null, 8, _hoisted_3$1)) : (openBlock(), createElementBlock("div", _hoisted_4$1, "No metadata available"))
|
||||
], 512)
|
||||
]);
|
||||
};
|
||||
@@ -15137,8 +15138,7 @@ function useAutocomplete(textareaRef, modelType = "loras", options = {}) {
|
||||
};
|
||||
}
|
||||
const _hoisted_1$1 = { class: "autocomplete-text-widget" };
|
||||
const _hoisted_2$1 = { class: "input-wrapper" };
|
||||
const _hoisted_3$1 = ["placeholder", "spellcheck"];
|
||||
const _hoisted_2$1 = ["placeholder", "spellcheck"];
|
||||
const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
||||
__name: "AutocompleteTextWidget",
|
||||
props: {
|
||||
@@ -15156,8 +15156,35 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
||||
const onModeChange = (event) => {
|
||||
const customEvent = event;
|
||||
isVueDomMode.value = customEvent.detail.isVueDomMode;
|
||||
updateVScrollbarWidth();
|
||||
};
|
||||
const textareaRef = ref(null);
|
||||
const inputWrapperRef = ref(null);
|
||||
const vScrollbarWidth = ref(0);
|
||||
let scrollbarResizeObserver = null;
|
||||
const updateVScrollbarWidth = () => {
|
||||
const ta = textareaRef.value;
|
||||
if (!ta) return;
|
||||
const overflowsY = ta.scrollHeight > ta.clientHeight;
|
||||
vScrollbarWidth.value = overflowsY ? ta.offsetWidth - ta.clientWidth : 0;
|
||||
};
|
||||
const observeScrollbarWidth = () => {
|
||||
unobserveScrollbarWidth();
|
||||
const ta = textareaRef.value;
|
||||
if (!ta || typeof ResizeObserver === "undefined") {
|
||||
return;
|
||||
}
|
||||
scrollbarResizeObserver = new ResizeObserver(() => {
|
||||
updateVScrollbarWidth();
|
||||
});
|
||||
scrollbarResizeObserver.observe(ta);
|
||||
};
|
||||
const unobserveScrollbarWidth = () => {
|
||||
if (scrollbarResizeObserver) {
|
||||
scrollbarResizeObserver.disconnect();
|
||||
scrollbarResizeObserver = null;
|
||||
}
|
||||
};
|
||||
const hasText = ref(false);
|
||||
const showClearButton = computed(() => hasText.value);
|
||||
useAutocomplete(
|
||||
@@ -15169,6 +15196,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
||||
hasText.value = textareaRef.value ? textareaRef.value.value.length > 0 : false;
|
||||
};
|
||||
const onInput = (event) => {
|
||||
updateVScrollbarWidth();
|
||||
if (event.inputType === "historyUndo") {
|
||||
const ta = textareaRef.value;
|
||||
if (ta && ta.selectionStart === 0 && ta.selectionEnd === ta.value.length) {
|
||||
@@ -15211,12 +15239,14 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
||||
}
|
||||
};
|
||||
const onExternalValueChange = () => {
|
||||
updateVScrollbarWidth();
|
||||
updateHasTextState();
|
||||
};
|
||||
const setupWidgetOnSetValue = () => {
|
||||
if (props.widget) {
|
||||
props.widget.onSetValue = (value) => {
|
||||
hasText.value = value.length > 0;
|
||||
updateVScrollbarWidth();
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -15266,9 +15296,12 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
||||
props.widget.callback(textareaRef.value.value);
|
||||
}
|
||||
setupWidgetOnSetValue();
|
||||
updateVScrollbarWidth();
|
||||
observeScrollbarWidth();
|
||||
document.addEventListener("lora-manager:vue-mode-change", onModeChange);
|
||||
});
|
||||
onUnmounted(() => {
|
||||
unobserveScrollbarWidth();
|
||||
if (props.widget.inputEl === textareaRef.value) {
|
||||
props.widget.inputEl = void 0;
|
||||
}
|
||||
@@ -15285,7 +15318,12 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
||||
});
|
||||
return (_ctx, _cache) => {
|
||||
return openBlock(), createElementBlock("div", _hoisted_1$1, [
|
||||
createBaseVNode("div", _hoisted_2$1, [
|
||||
createBaseVNode("div", {
|
||||
ref_key: "inputWrapperRef",
|
||||
ref: inputWrapperRef,
|
||||
class: "input-wrapper",
|
||||
style: normalizeStyle({ "--lm-vscrollbar-width": vScrollbarWidth.value + "px" })
|
||||
}, [
|
||||
createBaseVNode("textarea", {
|
||||
ref_key: "textareaRef",
|
||||
ref: textareaRef,
|
||||
@@ -15296,7 +15334,7 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
||||
"data-capture-wheel": "true",
|
||||
onInput,
|
||||
onWheel
|
||||
}, null, 46, _hoisted_3$1),
|
||||
}, null, 46, _hoisted_2$1),
|
||||
showClearButton.value ? (openBlock(), createElementBlock("button", {
|
||||
key: 0,
|
||||
type: "button",
|
||||
@@ -15324,12 +15362,12 @@ const _sfc_main$1 = /* @__PURE__ */ defineComponent({
|
||||
})
|
||||
], -1)
|
||||
])])) : createCommentVNode("", true)
|
||||
])
|
||||
], 4)
|
||||
]);
|
||||
};
|
||||
}
|
||||
});
|
||||
const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-4e322fec"]]);
|
||||
const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-793d67d2"]]);
|
||||
const _hoisted_1 = { class: "lora-info-tabs" };
|
||||
const _hoisted_2 = { class: "tab-content notes-tab" };
|
||||
const _hoisted_3 = { class: "info-field" };
|
||||
@@ -15746,12 +15784,6 @@ function createModeChangeCallback(node, updateDownstreamLoaders2, nodeSpecificCa
|
||||
updateDownstreamLoaders2(node);
|
||||
};
|
||||
}
|
||||
const app = {};
|
||||
const api = {
|
||||
fetchApi: (...args) => fetch(...args),
|
||||
addEventListener: (eventName, handler) => document.addEventListener(eventName, handler),
|
||||
removeEventListener: (eventName, handler) => document.removeEventListener(eventName, handler)
|
||||
};
|
||||
let _loraSyntaxFormatCache = null;
|
||||
let _loraSyntaxFormatRefreshPromise = null;
|
||||
async function _fetchLoraSyntaxFormat() {
|
||||
@@ -16108,8 +16140,8 @@ const AUTOCOMPLETE_TEXT_MIN_WIDTH_DEFAULT = 400;
|
||||
const AUTOCOMPLETE_TEXT_MIN_HEIGHT_DEFAULT = 300;
|
||||
const AUTOCOMPLETE_METADATA_VERSION = 1;
|
||||
const LORA_MANAGER_WIDGET_IDS_PROPERTY = "__lm_widget_ids";
|
||||
const originalGraphToPrompt = app$1.graphToPrompt.bind(app$1);
|
||||
app$1.graphToPrompt = async (...args) => {
|
||||
const originalGraphToPrompt = app.graphToPrompt.bind(app);
|
||||
app.graphToPrompt = async (...args) => {
|
||||
const result = await originalGraphToPrompt(...args);
|
||||
stripAutocompleteMetadataFromPromptResult(result);
|
||||
return result;
|
||||
@@ -16118,7 +16150,7 @@ function forwardMiddleMouseToCanvas(container) {
|
||||
if (!container) return;
|
||||
container.addEventListener("pointerdown", (event) => {
|
||||
if (event.button === 1) {
|
||||
const canvas = app$1.canvas;
|
||||
const canvas = app.canvas;
|
||||
if (canvas && typeof canvas.processMouseDown === "function") {
|
||||
canvas.processMouseDown(event);
|
||||
}
|
||||
@@ -16126,7 +16158,7 @@ function forwardMiddleMouseToCanvas(container) {
|
||||
});
|
||||
container.addEventListener("pointermove", (event) => {
|
||||
if ((event.buttons & 4) === 4) {
|
||||
const canvas = app$1.canvas;
|
||||
const canvas = app.canvas;
|
||||
if (canvas && typeof canvas.processMouseMove === "function") {
|
||||
canvas.processMouseMove(event);
|
||||
}
|
||||
@@ -16134,7 +16166,7 @@ function forwardMiddleMouseToCanvas(container) {
|
||||
});
|
||||
container.addEventListener("pointerup", (event) => {
|
||||
if (event.button === 1) {
|
||||
const canvas = app$1.canvas;
|
||||
const canvas = app.canvas;
|
||||
if (canvas && typeof canvas.processMouseUp === "function") {
|
||||
canvas.processMouseUp(event);
|
||||
}
|
||||
@@ -16252,7 +16284,7 @@ function createLoraRandomizerWidget(node) {
|
||||
const vueApp = createApp(LoraRandomizerWidget, {
|
||||
widget,
|
||||
node,
|
||||
api: api$1
|
||||
api
|
||||
});
|
||||
vueApp.use(PrimeVue, {
|
||||
unstyled: true,
|
||||
@@ -16327,7 +16359,7 @@ function createLoraCyclerWidget(node) {
|
||||
const vueApp = createApp(LoraCyclerWidget, {
|
||||
widget,
|
||||
node,
|
||||
api: api$1
|
||||
api
|
||||
});
|
||||
vueApp.use(PrimeVue, {
|
||||
unstyled: true,
|
||||
@@ -16515,13 +16547,13 @@ function applyAutocompleteTextLayoutFix(widget, _container, isVueMode) {
|
||||
}
|
||||
const initVueDomModeListener = () => {
|
||||
var _a2, _b;
|
||||
if ((_b = (_a2 = app$1.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.addEventListener) {
|
||||
app$1.ui.settings.addEventListener("Comfy.VueNodes.Enabled.change", () => {
|
||||
if ((_b = (_a2 = app.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.addEventListener) {
|
||||
app.ui.settings.addEventListener("Comfy.VueNodes.Enabled.change", () => {
|
||||
requestAnimationFrame(() => {
|
||||
var _a3, _b2, _c, _d, _e2, _f;
|
||||
const isVueDomMode = ((_c = (_b2 = (_a3 = app$1.ui) == null ? void 0 : _a3.settings) == null ? void 0 : _b2.getSettingValue) == null ? void 0 : _c.call(_b2, "Comfy.VueNodes.Enabled")) ?? false;
|
||||
if ((_d = app$1.graph) == null ? void 0 : _d.nodes) {
|
||||
for (const node of app$1.graph.nodes) {
|
||||
const isVueDomMode = ((_c = (_b2 = (_a3 = app.ui) == null ? void 0 : _a3.settings) == null ? void 0 : _b2.getSettingValue) == null ? void 0 : _c.call(_b2, "Comfy.VueNodes.Enabled")) ?? false;
|
||||
if ((_d = app.graph) == null ? void 0 : _d.nodes) {
|
||||
for (const node of app.graph.nodes) {
|
||||
const textWidget = (_e2 = node.widgets) == null ? void 0 : _e2.find(
|
||||
(w2) => w2.type === "AUTOCOMPLETE_TEXT_LORAS"
|
||||
);
|
||||
@@ -16536,7 +16568,7 @@ const initVueDomModeListener = () => {
|
||||
const grid = nodeEl.querySelector('[data-testid="node-widgets"]');
|
||||
if (!grid) continue;
|
||||
const nodeId = nodeEl.getAttribute("data-node-id");
|
||||
const node = (_a4 = app$1.graph) == null ? void 0 : _a4.getNodeById(nodeId);
|
||||
const node = (_a4 = app.graph) == null ? void 0 : _a4.getNodeById(nodeId);
|
||||
if (!node) continue;
|
||||
const rows = [];
|
||||
let needsFix = false;
|
||||
@@ -16557,7 +16589,7 @@ const initVueDomModeListener = () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
(_f = app$1.canvas) == null ? void 0 : _f.setDirty(true, true);
|
||||
(_f = app.canvas) == null ? void 0 : _f.setDirty(true, true);
|
||||
document.dispatchEvent(new CustomEvent("lora-manager:vue-mode-change", {
|
||||
detail: { isVueDomMode }
|
||||
}));
|
||||
@@ -16565,12 +16597,12 @@ const initVueDomModeListener = () => {
|
||||
});
|
||||
}
|
||||
};
|
||||
if ((_a = app$1.ui) == null ? void 0 : _a.settings) {
|
||||
if ((_a = app.ui) == null ? void 0 : _a.settings) {
|
||||
initVueDomModeListener();
|
||||
} else {
|
||||
const checkAppReady = setInterval(() => {
|
||||
var _a2;
|
||||
if ((_a2 = app$1.ui) == null ? void 0 : _a2.settings) {
|
||||
if ((_a2 = app.ui) == null ? void 0 : _a2.settings) {
|
||||
initVueDomModeListener();
|
||||
clearInterval(checkAppReady);
|
||||
}
|
||||
@@ -16609,8 +16641,8 @@ function createLoraInfoWidget(node) {
|
||||
const vueApp = createApp(LoraInfoWidget, {
|
||||
widget,
|
||||
node,
|
||||
api: api$1,
|
||||
app: app$1,
|
||||
api,
|
||||
app,
|
||||
isVueMode: typeof LiteGraph !== "undefined" && LiteGraph.vueNodesMode
|
||||
});
|
||||
vueApp.use(PrimeVue, {
|
||||
@@ -16705,7 +16737,7 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
|
||||
}
|
||||
);
|
||||
widget.metadataWidget = metadataWidget;
|
||||
const spellcheck = ((_c = (_b = (_a2 = app$1.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.getSettingValue) == null ? void 0 : _c.call(_b, "Comfy.TextareaWidget.Spellcheck")) ?? false;
|
||||
const spellcheck = ((_c = (_b = (_a2 = app.ui) == null ? void 0 : _a2.settings) == null ? void 0 : _b.getSettingValue) == null ? void 0 : _c.call(_b, "Comfy.TextareaWidget.Spellcheck")) ?? false;
|
||||
const maxHeight = modelType === "loras" ? AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT : void 0;
|
||||
const vueApp = createApp(AutocompleteTextWidget, {
|
||||
widget,
|
||||
@@ -16743,7 +16775,7 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
|
||||
const minHeight = modelType === "loras" ? void 0 : AUTOCOMPLETE_TEXT_MIN_HEIGHT_DEFAULT;
|
||||
return { widget, minWidth, minHeight };
|
||||
}
|
||||
app$1.registerExtension({
|
||||
app.registerExtension({
|
||||
name: "LoraManager.VueWidgets",
|
||||
getCustomWidgets() {
|
||||
return {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user