Compare commits

..

4 Commits

Author SHA1 Message Date
Will Miao
60175334b5 chore(release): bump version to v1.0.9 2026-05-28 21:46:46 +08:00
Will Miao
f65a01df00 feat(recipe): add bulk Repair Metadata for Selected operation to recipes page
Adds a new bulk operation in the recipes page that allows users to select
multiple recipes and repair their metadata in batch.

Backend:
- New POST /api/lm/recipes/repair-bulk endpoint accepting recipe_ids array
- repair_recipes_bulk handler iterates repair_recipe_by_id for each recipe
- Response includes per-recipe updated data for frontend card refresh

Frontend:
- Bulk context menu: new 'Repair Metadata for Selected' item in Metadata section
- BulkManager.repairSelectedRecipes() with loading/toast flow
- Uses VirtualScroller.updateSingleItem() per repaired recipe (no full reload)
- Visibility controlled via repairMetadata actionConfig flag

Locales:
- Added repairMetadata, repairBulkComplete, repairBulkSkipped, repairBulkFailed
- Translated across all 9 supported languages
2026-05-28 20:16:59 +08:00
Will Miao
430e24d70b fix(ui): hide skip-metadata-refresh bulk menu items for recipes 2026-05-28 19:11:49 +08:00
Will Miao
14f0c48fdd fix(recipe): detect and repair corrupted checkpoints in repair flow
Add corruption detection to _repair_single_recipe: if checkpoint.modelVersionId matches any LoRA's modelVersionId, the checkpoint is corrupted (a LoRA was saved as checkpoint). Clear the checkpoint and remove the matching LoRA entry, then let enrichment re-resolve the correct checkpoint from CivitAI metadata.

This fixes the retroactive repair path for the modelVersionIds[0] fallback bug.
2026-05-28 17:19:27 +08:00
19 changed files with 313 additions and 62 deletions

View File

@@ -21,7 +21,9 @@
"stone9k", "stone9k",
"Rosenthal", "Rosenthal",
"Francisco Tatis", "Francisco Tatis",
"JongWon Han",
"runte3221", "runte3221",
"FreelancerZ",
"Fraser Cross", "Fraser Cross",
"Polymorphic Indeterminate", "Polymorphic Indeterminate",
"Marc Whiffen", "Marc Whiffen",
@@ -43,11 +45,13 @@
"ClockDaemon", "ClockDaemon",
"KD", "KD",
"Omnidex", "Omnidex",
"Tyler Trebuchon",
"Release Cabrakan", "Release Cabrakan",
"Tobi_Swagg", "Tobi_Swagg",
"SG", "SG",
"James Dooley", "James Dooley",
"zenbound", "zenbound",
"Buzzard",
"jmack", "jmack",
"Andrew Wilson", "Andrew Wilson",
"Greybush", "Greybush",
@@ -57,7 +61,7 @@
"Wolffen", "Wolffen",
"Ricky Carter", "Ricky Carter",
"James Todd", "James Todd",
"JongWon Han", "Steven Pfeiffer",
"VantAI", "VantAI",
"Tim", "Tim",
"Lisster", "Lisster",
@@ -65,7 +69,6 @@
"Illrigger", "Illrigger",
"Tom Corrigan", "Tom Corrigan",
"JackieWang", "JackieWang",
"FreelancerZ",
"fnkylove", "fnkylove",
"Yushio", "Yushio",
"Vik71it", "Vik71it",
@@ -73,6 +76,7 @@
"Lilleman", "Lilleman",
"Robert Stacey", "Robert Stacey",
"PM", "PM",
"Todd Keck",
"Edgar Tejeda", "Edgar Tejeda",
"Jorge Hussni", "Jorge Hussni",
"Liam MacDougal", "Liam MacDougal",
@@ -91,7 +95,6 @@
"Melville Parrish", "Melville Parrish",
"daniel dove", "daniel dove",
"Lustre", "Lustre",
"Tyler Trebuchon",
"JW Sin", "JW Sin",
"contrite831", "contrite831",
"Alex", "Alex",
@@ -99,20 +102,19 @@
"carozzz", "carozzz",
"Marlon Daniels", "Marlon Daniels",
"Starkselle", "Starkselle",
"Buzzard",
"Aaron Bleuer", "Aaron Bleuer",
"LacesOut!", "LacesOut!",
"greebles", "greebles",
"Adam Shaw", "Adam Shaw",
"Anthony Rizzo", "Anthony Rizzo",
"M Postkasse",
"Gooohokrbe", "Gooohokrbe",
"RedrockVP", "RedrockVP",
"ASLPro3D", "Wicked Choices by ASLPro3D",
"OldBones", "OldBones",
"Jacob Hoehler", "Jacob Hoehler",
"FinalyFree", "FinalyFree",
"Weasyl", "Weasyl",
"Steven Pfeiffer",
"Timmy", "Timmy",
"Johnny", "Johnny",
"Cory Paza", "Cory Paza",
@@ -126,7 +128,7 @@
"corde", "corde",
"Nick Walker", "Nick Walker",
"Bishoujoker", "Bishoujoker",
"Todd Keck", "aai",
"Briton Heilbrun", "Briton Heilbrun",
"Tori", "Tori",
"wildnut", "wildnut",
@@ -153,12 +155,13 @@
"JaxMax", "JaxMax",
"takyamtom", "takyamtom",
"Jwk0205", "Jwk0205",
"Bro Xie",
"batblue", "batblue",
"carey6409", "carey6409",
"Olive", "Olive",
"太郎 ゲーム",
"Some Guy Named Barry", "Some Guy Named Barry",
"Cosmosis", "Cosmosis",
"M Postkasse",
"AELOX", "AELOX",
"Nicfit23", "Nicfit23",
"FloPro4Sho", "FloPro4Sho",
@@ -172,13 +175,13 @@
"Serge Bekenkamp", "Serge Bekenkamp",
"Jimmy Ledbetter", "Jimmy Ledbetter",
"LeoZero", "LeoZero",
"Antonio Pontes",
"ApathyJones", "ApathyJones",
"Julian V", "Julian V",
"Steven Owens", "Steven Owens",
"nahinahi9", "nahinahi9",
"Dustin Chen", "Dustin Chen",
"dan", "dan",
"aai",
"Mouthlessman", "Mouthlessman",
"otaku fra", "otaku fra",
"ViperC", "ViperC",
@@ -199,15 +202,15 @@
"Jon Sandman", "Jon Sandman",
"Ubivis", "Ubivis",
"CloudValley", "CloudValley",
"linnfrey",
"IamAyam", "IamAyam",
"skaterb949",
"Joboshy", "Joboshy",
"Bohemian Corporal", "Bohemian Corporal",
"Dan", "Dan",
"confiscated Zyra", "confiscated Zyra",
"Bro Xie",
"yer fey", "yer fey",
"Error_Rule34_Not_found", "Error_Rule34_Not_found",
"太郎 ゲーム",
"Roslynd", "Roslynd",
"Tee Gee", "Tee Gee",
"jinxedx", "jinxedx",
@@ -221,7 +224,7 @@
"Magic Noob", "Magic Noob",
"Pronredn", "Pronredn",
"DougPeterson", "DougPeterson",
"Antonio Pontes", "Jeff",
"Bruce", "Bruce",
"lh qwe", "lh qwe",
"Kevin John Duck", "Kevin John Duck",
@@ -249,19 +252,21 @@
"地獄の禄", "地獄の禄",
"MJG", "MJG",
"David LaVallee", "David LaVallee",
"linnfrey",
"ae", "ae",
"Tr4shP4nda", "Tr4shP4nda",
"WRL_SPR", "WRL_SPR",
"capn", "capn",
"Joseph", "Joseph",
"Mirko Katzula", "Mirko Katzula",
"dan",
"Piccio08",
"kumakichi",
"cppbel",
"奚明 刘", "奚明 刘",
"Brian M", "Brian M",
"Josef Lanzl", "Josef Lanzl",
"Nerezza", "Nerezza",
"sanborondon", "sanborondon",
"Griffin Dahlberg",
"준희 김", "준희 김",
"Taylor Funk", "Taylor Funk",
"aezin", "aezin",
@@ -278,10 +283,11 @@
"Noora", "Noora",
"Pierce McBride", "Pierce McBride",
"Mattssn", "Mattssn",
"Mikko Hemilä",
"Jamie Ogletree", "Jamie Ogletree",
"a _", "a _",
"Jeff",
"James Coleman", "James Coleman",
"Martial",
"Emil Andersson", "Emil Andersson",
"Ouro Boros", "Ouro Boros",
"Chad Idk", "Chad Idk",
@@ -302,10 +308,6 @@
"Nick “Loadstone” D", "Nick “Loadstone” D",
"Gamalonia", "Gamalonia",
"momokai", "momokai",
"dan",
"Piccio08",
"kumakichi",
"cppbel",
"starbugx", "starbugx",
"Moon Knight", "Moon Knight",
"몽타주", "몽타주",
@@ -337,6 +339,7 @@
"Andrew", "Andrew",
"Robert Wegemund", "Robert Wegemund",
"Littlehuggy", "Littlehuggy",
"Gregory Kozhemiak",
"Draven T", "Draven T",
"mrjuan", "mrjuan",
"Brian Buie", "Brian Buie",
@@ -350,7 +353,6 @@
"Joshua Gray", "Joshua Gray",
"Morgandel", "Morgandel",
"Focuschannel", "Focuschannel",
"Mikko Hemilä",
"Noah", "Noah",
"Jacob McDaniel", "Jacob McDaniel",
"X", "X",
@@ -359,7 +361,6 @@
"Artokun", "Artokun",
"Michael Taylor", "Michael Taylor",
"Derek Baker", "Derek Baker",
"Martial",
"Anthony Faxlandez", "Anthony Faxlandez",
"battu", "battu",
"Michael Anthony Scott", "Michael Anthony Scott",
@@ -367,8 +368,6 @@
"Decx _", "Decx _",
"Pat Hen", "Pat Hen",
"Jordan Shaw", "Jordan Shaw",
"Thesharingbrother",
"ResidentDeviant",
"四糸凜音", "四糸凜音",
"Nihongasuki", "Nihongasuki",
"JC", "JC",
@@ -412,11 +411,11 @@
"Wolfe7D1", "Wolfe7D1",
"blikkies", "blikkies",
"Chris", "Chris",
"Gregory Kozhemiak",
"elleshar666", "elleshar666",
"Shock Shockor", "Shock Shockor",
"ACTUALLY_the_Real_Willem_Dafoe", "ACTUALLY_the_Real_Willem_Dafoe",
"Goldwaters", "Goldwaters",
"Kauffy",
"Zude", "Zude",
"John J Linehan", "John J Linehan",
"Kyler", "Kyler",
@@ -426,19 +425,21 @@
"Justin Blaylock", "Justin Blaylock",
"aRtFuL_DodGeR", "aRtFuL_DodGeR",
"Vane Holzer", "Vane Holzer",
"psytrax",
"hexxish", "hexxish",
"notedfakes", "notedfakes",
"DarkSunset",
"Nathan", "Nathan",
"Billy Gladky", "Billy Gladky",
"NICHOLAS BAXLEY", "NICHOLAS BAXLEY",
"Michael Scott", "Michael Scott",
"Probis", "Probis",
"Ed Wang", "Ed Wang",
"Wes Sims",
"ItsGeneralButtNaked", "ItsGeneralButtNaked",
"SRDB", "SRDB",
"g unit", "g unit",
"Distortik", "Distortik",
"Filippo Ferrari",
"Youguang", "Youguang",
"Saya", "Saya",
"andrewzpong", "andrewzpong",
@@ -456,6 +457,7 @@
"emadsultan", "emadsultan",
"Pkrsky", "Pkrsky",
"nanana", "nanana",
"FeralOpticsAI",
"Pavlaki", "Pavlaki",
"Doug+Rintoul", "Doug+Rintoul",
"Noor", "Noor",
@@ -483,7 +485,6 @@
"Time Valentine", "Time Valentine",
"Михал Михалыч", "Михал Михалыч",
"Matt", "Matt",
"Kauffy",
"Frogmilk", "Frogmilk",
"SPJ", "SPJ",
"Kyron Mahan", "Kyron Mahan",
@@ -491,11 +492,11 @@
"Nick Kage", "Nick Kage",
"TBitz33", "TBitz33",
"Anonym dkjglfleeoeldldldlkf", "Anonym dkjglfleeoeldldldlkf",
"psytrax",
"Cyrus Fett", "Cyrus Fett",
"Ezokewn", "Ezokewn",
"SendingRavens", "SendingRavens",
"Xenon Xue", "Xenon Xue",
"JackJohnnyJim",
"Edward Ten Eyck", "Edward Ten Eyck",
"Michael Docherty", "Michael Docherty",
"Paul Hartsuyker", "Paul Hartsuyker",
@@ -504,15 +505,14 @@
"Solixer", "Solixer",
"Jacob Winter", "Jacob Winter",
"Ryan Presley Ng", "Ryan Presley Ng",
"Wes Sims",
"jinksta187", "jinksta187",
"Donor4115", "Donor4115",
"Manu Thetug", "Manu Thetug",
"Karlanx",
"Lyavph", "Lyavph",
"David", "David",
"Meilo", "Meilo",
"operationancut", "operationancut",
"Filippo Ferrari",
"shinonomeiro", "shinonomeiro",
"Snille", "Snille",
"MaartenAlbers", "MaartenAlbers",
@@ -531,6 +531,8 @@
"Scott", "Scott",
"Muratoraccio", "Muratoraccio",
"D", "D",
"YassineKhaled",
"Y",
"MatteKey", "MatteKey",
"Flob", "Flob",
"ShiroSenpai", "ShiroSenpai",
@@ -552,7 +554,6 @@
"rsamerica", "rsamerica",
"sfasdfasfdsa", "sfasdfasfdsa",
"Alan+Cano", "Alan+Cano",
"FeralOpticsAI",
"generic404", "generic404",
"abattoirblues", "abattoirblues",
"zounik", "zounik",
@@ -584,7 +585,6 @@
"Sauv", "Sauv",
"Steven", "Steven",
"CptNeo", "CptNeo",
"JackJohnnyJim",
"TenaciousD", "TenaciousD",
"Dmitry Ryzhov", "Dmitry Ryzhov",
"Khánh Đặng", "Khánh Đặng",
@@ -599,7 +599,6 @@
"Andrew Wilkinson", "Andrew Wilkinson",
"Yavizu3d", "Yavizu3d",
"Maxim", "Maxim",
"Karlanx",
"Yves Poezevara", "Yves Poezevara",
"Teriak47", "Teriak47",
"Just me", "Just me",
@@ -637,6 +636,7 @@
"Captain_Swag", "Captain_Swag",
"obkircher", "obkircher",
"gwyar", "gwyar",
"ResidentDeviant",
"D", "D",
"edgecase", "edgecase",
"Neoxena", "Neoxena",
@@ -681,8 +681,6 @@
"low9", "low9",
"Winged", "Winged",
"you+halo9", "you+halo9",
"YassineKhaled",
"YK12",
"Somebody", "Somebody",
"Somebody", "Somebody",
"Crescent~San", "Crescent~San",
@@ -697,6 +695,7 @@
"Coeur+de+cochon", "Coeur+de+cochon",
"Obsidian.Studios", "Obsidian.Studios",
"han b", "han b",
"Zomba Mann",
"Nico", "Nico",
"Maximilian Krischan", "Maximilian Krischan",
"Banana Joe", "Banana Joe",
@@ -714,7 +713,6 @@
"Ronan Delevacq", "Ronan Delevacq",
"karim ben brik", "karim ben brik",
"Vinarus", "Vinarus",
"james",
"Michael Zhu", "Michael Zhu",
"Nemisu", "Nemisu",
"Seraphy", "Seraphy",
@@ -743,9 +741,11 @@
"dsffsdfsdfsdfsdfsdf", "dsffsdfsdfsdfsdfsdf",
"somethingtosay8", "somethingtosay8",
"Jean-françois SEMA", "Jean-françois SEMA",
"3zS4QNQ4",
"Terminuz", "Terminuz",
"Kurt", "Kurt",
"ivistorm", "ivistorm",
"Ivan Imes",
"Faburizu", "Faburizu",
"Jack Lawfield", "Jack Lawfield",
"jimyjomson", "jimyjomson",

View File

@@ -689,6 +689,7 @@
"setContentRating": "Inhaltsbewertung für alle festlegen", "setContentRating": "Inhaltsbewertung für alle festlegen",
"copyAll": "Alle Syntax kopieren", "copyAll": "Alle Syntax kopieren",
"refreshAll": "Alle Metadaten aktualisieren", "refreshAll": "Alle Metadaten aktualisieren",
"repairMetadata": "Metadaten der Auswahl reparieren",
"checkUpdates": "Auswahl auf Updates prüfen", "checkUpdates": "Auswahl auf Updates prüfen",
"moveAll": "Alle in Ordner verschieben", "moveAll": "Alle in Ordner verschieben",
"autoOrganize": "Automatisch organisieren", "autoOrganize": "Automatisch organisieren",
@@ -1693,6 +1694,9 @@
"batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportBrowseFailed": "Failed to browse directory: {message}",
"batchImportDirectorySelected": "Directory selected: {path}", "batchImportDirectorySelected": "Directory selected: {path}",
"noRecipesSelected": "Keine Rezepte ausgewählt", "noRecipesSelected": "Keine Rezepte ausgewählt",
"repairBulkComplete": "Reparatur abgeschlossen: {repaired} repariert, {skipped} übersprungen (von {total})",
"repairBulkSkipped": "Keine Reparatur für die {total} ausgewählten Rezepte erforderlich",
"repairBulkFailed": "Reparatur der ausgewählten Rezepte fehlgeschlagen: {message}",
"noMissingLorasInSelection": "Keine fehlenden LoRAs in ausgewählten Rezepten gefunden", "noMissingLorasInSelection": "Keine fehlenden LoRAs in ausgewählten Rezepten gefunden",
"noLoraRootConfigured": "Kein LoRA-Stammverzeichnis konfiguriert. Bitte legen Sie ein Standard-LoRA-Stammverzeichnis in den Einstellungen fest." "noLoraRootConfigured": "Kein LoRA-Stammverzeichnis konfiguriert. Bitte legen Sie ein Standard-LoRA-Stammverzeichnis in den Einstellungen fest."
}, },

View File

@@ -689,6 +689,7 @@
"setContentRating": "Set Content Rating for Selected", "setContentRating": "Set Content Rating for Selected",
"copyAll": "Copy Selected Syntax", "copyAll": "Copy Selected Syntax",
"refreshAll": "Refresh Selected Metadata", "refreshAll": "Refresh Selected Metadata",
"repairMetadata": "Repair Metadata for Selected",
"checkUpdates": "Check Updates for Selected", "checkUpdates": "Check Updates for Selected",
"moveAll": "Move Selected to Folder", "moveAll": "Move Selected to Folder",
"autoOrganize": "Auto-Organize Selected", "autoOrganize": "Auto-Organize Selected",
@@ -1693,6 +1694,9 @@
"batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportBrowseFailed": "Failed to browse directory: {message}",
"batchImportDirectorySelected": "Directory selected: {path}", "batchImportDirectorySelected": "Directory selected: {path}",
"noRecipesSelected": "No recipes selected", "noRecipesSelected": "No recipes selected",
"repairBulkComplete": "Repair complete: {repaired} repaired, {skipped} skipped (of {total})",
"repairBulkSkipped": "No repair needed for any of the {total} selected recipes",
"repairBulkFailed": "Failed to repair selected recipes: {message}",
"noMissingLorasInSelection": "No missing LoRAs found in selected recipes", "noMissingLorasInSelection": "No missing LoRAs found in selected recipes",
"noLoraRootConfigured": "No LoRA root directory configured. Please set a default LoRA root in settings." "noLoraRootConfigured": "No LoRA root directory configured. Please set a default LoRA root in settings."
}, },

View File

@@ -689,6 +689,7 @@
"setContentRating": "Establecer clasificación de contenido para todos", "setContentRating": "Establecer clasificación de contenido para todos",
"copyAll": "Copiar toda la sintaxis", "copyAll": "Copiar toda la sintaxis",
"refreshAll": "Actualizar todos los metadatos", "refreshAll": "Actualizar todos los metadatos",
"repairMetadata": "Reparar metadatos de la selección",
"checkUpdates": "Comprobar actualizaciones para la selección", "checkUpdates": "Comprobar actualizaciones para la selección",
"moveAll": "Mover todos a carpeta", "moveAll": "Mover todos a carpeta",
"autoOrganize": "Auto-organizar seleccionados", "autoOrganize": "Auto-organizar seleccionados",
@@ -1693,6 +1694,9 @@
"batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportBrowseFailed": "Failed to browse directory: {message}",
"batchImportDirectorySelected": "Directory selected: {path}", "batchImportDirectorySelected": "Directory selected: {path}",
"noRecipesSelected": "No se han seleccionado recetas", "noRecipesSelected": "No se han seleccionado recetas",
"repairBulkComplete": "Reparación completa: {repaired} reparadas, {skipped} omitidas (de {total})",
"repairBulkSkipped": "No se necesita reparación para ninguna de las {total} recetas seleccionadas",
"repairBulkFailed": "Error al reparar las recetas seleccionadas: {message}",
"noMissingLorasInSelection": "No se encontraron LoRAs faltantes en las recetas seleccionadas", "noMissingLorasInSelection": "No se encontraron LoRAs faltantes en las recetas seleccionadas",
"noLoraRootConfigured": "No se ha configurado el directorio raíz de LoRA. Por favor, establezca un directorio raíz de LoRA predeterminado en la configuración." "noLoraRootConfigured": "No se ha configurado el directorio raíz de LoRA. Por favor, establezca un directorio raíz de LoRA predeterminado en la configuración."
}, },

View File

@@ -689,6 +689,7 @@
"setContentRating": "Définir la classification du contenu pour tous", "setContentRating": "Définir la classification du contenu pour tous",
"copyAll": "Copier toute la syntaxe", "copyAll": "Copier toute la syntaxe",
"refreshAll": "Actualiser toutes les métadonnées", "refreshAll": "Actualiser toutes les métadonnées",
"repairMetadata": "Réparer les métadonnées de la sélection",
"checkUpdates": "Vérifier les mises à jour pour la sélection", "checkUpdates": "Vérifier les mises à jour pour la sélection",
"moveAll": "Déplacer tout vers un dossier", "moveAll": "Déplacer tout vers un dossier",
"autoOrganize": "Auto-organiser la sélection", "autoOrganize": "Auto-organiser la sélection",
@@ -1693,6 +1694,9 @@
"batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportBrowseFailed": "Failed to browse directory: {message}",
"batchImportDirectorySelected": "Directory selected: {path}", "batchImportDirectorySelected": "Directory selected: {path}",
"noRecipesSelected": "Aucune recette sélectionnée", "noRecipesSelected": "Aucune recette sélectionnée",
"repairBulkComplete": "Réparation terminée : {repaired} réparée(s), {skipped} ignorée(s) (sur {total})",
"repairBulkSkipped": "Aucune réparation nécessaire parmi les {total} recettes sélectionnées",
"repairBulkFailed": "Échec de la réparation des recettes sélectionnées : {message}",
"noMissingLorasInSelection": "Aucun LoRA manquant trouvé dans les recettes sélectionnées", "noMissingLorasInSelection": "Aucun LoRA manquant trouvé dans les recettes sélectionnées",
"noLoraRootConfigured": "Aucun répertoire racine LoRA configuré. Veuillez définir un répertoire racine LoRA par défaut dans les paramètres." "noLoraRootConfigured": "Aucun répertoire racine LoRA configuré. Veuillez définir un répertoire racine LoRA par défaut dans les paramètres."
}, },

View File

@@ -689,6 +689,7 @@
"setContentRating": "הגדר דירוג תוכן לכל המודלים", "setContentRating": "הגדר דירוג תוכן לכל המודלים",
"copyAll": "העתק את כל התחבירים", "copyAll": "העתק את כל התחבירים",
"refreshAll": "רענן את כל המטא-דאטה", "refreshAll": "רענן את כל המטא-דאטה",
"repairMetadata": "תקן מטא-דאטה עבור הנבחרים",
"checkUpdates": "בדוק עדכונים לבחירה", "checkUpdates": "בדוק עדכונים לבחירה",
"moveAll": "העבר הכל לתיקייה", "moveAll": "העבר הכל לתיקייה",
"autoOrganize": "ארגן אוטומטית נבחרים", "autoOrganize": "ארגן אוטומטית נבחרים",
@@ -1693,6 +1694,9 @@
"batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportBrowseFailed": "Failed to browse directory: {message}",
"batchImportDirectorySelected": "Directory selected: {path}", "batchImportDirectorySelected": "Directory selected: {path}",
"noRecipesSelected": "לא נבחרו מתכונים", "noRecipesSelected": "לא נבחרו מתכונים",
"repairBulkComplete": "התיקון הושלם: {repaired} תוקנו, {skipped} דולגו (מתוך {total})",
"repairBulkSkipped": "אין צורך בתיקון עבור {total} המתכונים הנבחרים",
"repairBulkFailed": "תיקון המתכונים הנבחרים נכשל: {message}",
"noMissingLorasInSelection": "לא נמצאו LoRAs חסרים במתכונים שנבחרו", "noMissingLorasInSelection": "לא נמצאו LoRAs חסרים במתכונים שנבחרו",
"noLoraRootConfigured": "תיקיית השורש של LoRA לא מוגדרת. אנא הגדר תיקיית שורש LoRA ברירת מחדל בהגדרות." "noLoraRootConfigured": "תיקיית השורש של LoRA לא מוגדרת. אנא הגדר תיקיית שורש LoRA ברירת מחדל בהגדרות."
}, },

View File

@@ -689,6 +689,7 @@
"setContentRating": "すべてのモデルのコンテンツレーティングを設定", "setContentRating": "すべてのモデルのコンテンツレーティングを設定",
"copyAll": "すべての構文をコピー", "copyAll": "すべての構文をコピー",
"refreshAll": "すべてのメタデータを更新", "refreshAll": "すべてのメタデータを更新",
"repairMetadata": "選択したレシピのメタデータを修復",
"checkUpdates": "選択項目の更新を確認", "checkUpdates": "選択項目の更新を確認",
"moveAll": "すべてをフォルダに移動", "moveAll": "すべてをフォルダに移動",
"autoOrganize": "自動整理を実行", "autoOrganize": "自動整理を実行",
@@ -1693,6 +1694,9 @@
"batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportBrowseFailed": "Failed to browse directory: {message}",
"batchImportDirectorySelected": "Directory selected: {path}", "batchImportDirectorySelected": "Directory selected: {path}",
"noRecipesSelected": "レシピが選択されていません", "noRecipesSelected": "レシピが選択されていません",
"repairBulkComplete": "修復完了:{repaired} 件修復、{skipped} 件スキップ(合計 {total} 件)",
"repairBulkSkipped": "選択した {total} 件のレシピは修復不要です",
"repairBulkFailed": "選択したレシピの修復に失敗しました:{message}",
"noMissingLorasInSelection": "選択したレシピに不足している LoRA が見つかりませんでした", "noMissingLorasInSelection": "選択したレシピに不足している LoRA が見つかりませんでした",
"noLoraRootConfigured": "LoRA ルートディレクトリが設定されていません。設定でデフォルトの LoRA ルートを設定してください。" "noLoraRootConfigured": "LoRA ルートディレクトリが設定されていません。設定でデフォルトの LoRA ルートを設定してください。"
}, },

View File

@@ -689,6 +689,7 @@
"setContentRating": "모든 모델에 콘텐츠 등급 설정", "setContentRating": "모든 모델에 콘텐츠 등급 설정",
"copyAll": "모든 문법 복사", "copyAll": "모든 문법 복사",
"refreshAll": "모든 메타데이터 새로고침", "refreshAll": "모든 메타데이터 새로고침",
"repairMetadata": "선택한 레시피 메타데이터 복구",
"checkUpdates": "선택 항목 업데이트 확인", "checkUpdates": "선택 항목 업데이트 확인",
"moveAll": "모두 폴더로 이동", "moveAll": "모두 폴더로 이동",
"autoOrganize": "자동 정리 선택", "autoOrganize": "자동 정리 선택",
@@ -1693,6 +1694,9 @@
"batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportBrowseFailed": "Failed to browse directory: {message}",
"batchImportDirectorySelected": "Directory selected: {path}", "batchImportDirectorySelected": "Directory selected: {path}",
"noRecipesSelected": "선택한 레시피가 없습니다", "noRecipesSelected": "선택한 레시피가 없습니다",
"repairBulkComplete": "복구 완료: {repaired}개 복구, {skipped}개 건너뜀 (총 {total}개)",
"repairBulkSkipped": "선택한 {total}개 레시피는 복구가 필요하지 않습니다",
"repairBulkFailed": "선택한 레시피 복구 실패: {message}",
"noMissingLorasInSelection": "선택한 레시피에서 누락된 LoRA를 찾을 수 없습니다", "noMissingLorasInSelection": "선택한 레시피에서 누락된 LoRA를 찾을 수 없습니다",
"noLoraRootConfigured": "LoRA 루트 디렉토리가 구성되지 않았습니다. 설정에서 기본 LoRA 루트를 설정하세요." "noLoraRootConfigured": "LoRA 루트 디렉토리가 구성되지 않았습니다. 설정에서 기본 LoRA 루트를 설정하세요."
}, },

View File

@@ -689,6 +689,7 @@
"setContentRating": "Установить рейтинг контента для всех", "setContentRating": "Установить рейтинг контента для всех",
"copyAll": "Копировать весь синтаксис", "copyAll": "Копировать весь синтаксис",
"refreshAll": "Обновить все метаданные", "refreshAll": "Обновить все метаданные",
"repairMetadata": "Восстановить метаданные для выбранных",
"checkUpdates": "Проверить обновления для выбранных", "checkUpdates": "Проверить обновления для выбранных",
"moveAll": "Переместить все в папку", "moveAll": "Переместить все в папку",
"autoOrganize": "Автоматически организовать выбранные", "autoOrganize": "Автоматически организовать выбранные",
@@ -1693,6 +1694,9 @@
"batchImportBrowseFailed": "Failed to browse directory: {message}", "batchImportBrowseFailed": "Failed to browse directory: {message}",
"batchImportDirectorySelected": "Directory selected: {path}", "batchImportDirectorySelected": "Directory selected: {path}",
"noRecipesSelected": "Рецепты не выбраны", "noRecipesSelected": "Рецепты не выбраны",
"repairBulkComplete": "Восстановление завершено: {repaired} восстановлено, {skipped} пропущено (из {total})",
"repairBulkSkipped": "Ни один из {total} выбранных рецептов не требует восстановления",
"repairBulkFailed": "Не удалось восстановить выбранные рецепты: {message}",
"noMissingLorasInSelection": "В выбранных рецептах не найдены отсутствующие LoRAs", "noMissingLorasInSelection": "В выбранных рецептах не найдены отсутствующие LoRAs",
"noLoraRootConfigured": "Корневой каталог LoRA не настроен. Пожалуйста, установите корневой каталог LoRA по умолчанию в настройках." "noLoraRootConfigured": "Корневой каталог LoRA не настроен. Пожалуйста, установите корневой каталог LoRA по умолчанию в настройках."
}, },

View File

@@ -689,6 +689,7 @@
"setContentRating": "为所选中设置内容评级", "setContentRating": "为所选中设置内容评级",
"copyAll": "复制所选中语法", "copyAll": "复制所选中语法",
"refreshAll": "刷新所选中元数据", "refreshAll": "刷新所选中元数据",
"repairMetadata": "修复所选中元数据",
"checkUpdates": "检查所选更新", "checkUpdates": "检查所选更新",
"moveAll": "移动所选中到文件夹", "moveAll": "移动所选中到文件夹",
"autoOrganize": "自动整理所选模型", "autoOrganize": "自动整理所选模型",
@@ -1693,6 +1694,9 @@
"batchImportBrowseFailed": "浏览目录失败:{message}", "batchImportBrowseFailed": "浏览目录失败:{message}",
"batchImportDirectorySelected": "已选择目录:{path}", "batchImportDirectorySelected": "已选择目录:{path}",
"noRecipesSelected": "未选择任何配方", "noRecipesSelected": "未选择任何配方",
"repairBulkComplete": "修复完成:{repaired} 个已修复,{skipped} 个已跳过(共 {total} 个)",
"repairBulkSkipped": "所选 {total} 个配方无需修复",
"repairBulkFailed": "修复所选配方失败:{message}",
"noMissingLorasInSelection": "在选定的配方中未找到缺失的 LoRAs", "noMissingLorasInSelection": "在选定的配方中未找到缺失的 LoRAs",
"noLoraRootConfigured": "未配置 LoRA 根目录。请在设置中设置默认的 LoRA 根目录。" "noLoraRootConfigured": "未配置 LoRA 根目录。请在设置中设置默认的 LoRA 根目录。"
}, },

View File

@@ -689,6 +689,7 @@
"setContentRating": "為全部設定內容分級", "setContentRating": "為全部設定內容分級",
"copyAll": "複製全部語法", "copyAll": "複製全部語法",
"refreshAll": "刷新全部 metadata", "refreshAll": "刷新全部 metadata",
"repairMetadata": "修復所選中元數據",
"checkUpdates": "檢查所選更新", "checkUpdates": "檢查所選更新",
"moveAll": "全部移動到資料夾", "moveAll": "全部移動到資料夾",
"autoOrganize": "自動整理所選模型", "autoOrganize": "自動整理所選模型",
@@ -1693,6 +1694,9 @@
"batchImportBrowseFailed": "瀏覽目錄失敗:{message}", "batchImportBrowseFailed": "瀏覽目錄失敗:{message}",
"batchImportDirectorySelected": "已選擇目錄:{path}", "batchImportDirectorySelected": "已選擇目錄:{path}",
"noRecipesSelected": "未選取任何食譜", "noRecipesSelected": "未選取任何食譜",
"repairBulkComplete": "修復完成:{repaired} 個已修復,{skipped} 個已跳過(共 {total} 個)",
"repairBulkSkipped": "所選 {total} 個配方無需修復",
"repairBulkFailed": "修復所選配方失敗:{message}",
"noMissingLorasInSelection": "在選取的食譜中未找到缺失的 LoRAs", "noMissingLorasInSelection": "在選取的食譜中未找到缺失的 LoRAs",
"noLoraRootConfigured": "未配置 LoRA 根目錄。請在設定中設定預設的 LoRA 根目錄。" "noLoraRootConfigured": "未配置 LoRA 根目錄。請在設定中設定預設的 LoRA 根目錄。"
}, },

View File

@@ -87,6 +87,7 @@ class RecipeHandlerSet:
"repair_recipes": self.management.repair_recipes, "repair_recipes": self.management.repair_recipes,
"cancel_repair": self.management.cancel_repair, "cancel_repair": self.management.cancel_repair,
"repair_recipe": self.management.repair_recipe, "repair_recipe": self.management.repair_recipe,
"repair_recipes_bulk": self.management.repair_recipes_bulk,
"get_repair_progress": self.management.get_repair_progress, "get_repair_progress": self.management.get_repair_progress,
"start_batch_import": self.batch_import.start_batch_import, "start_batch_import": self.batch_import.start_batch_import,
"get_batch_import_progress": self.batch_import.get_batch_import_progress, "get_batch_import_progress": self.batch_import.get_batch_import_progress,
@@ -706,6 +707,69 @@ class RecipeManagementHandler:
self._logger.error("Error cancelling recipe repair: %s", exc, exc_info=True) self._logger.error("Error cancelling recipe repair: %s", exc, exc_info=True)
return web.json_response({"success": False, "error": str(exc)}, status=500) return web.json_response({"success": False, "error": str(exc)}, status=500)
async def repair_recipes_bulk(self, request: web.Request) -> web.Response:
"""Bulk repair metadata for multiple recipes by their IDs.
Accepts a JSON body with a "recipe_ids" array and iterates
repair_recipe_by_id over each entry, collecting statistics.
"""
try:
await self._ensure_dependencies_ready()
recipe_scanner = self._recipe_scanner_getter()
if recipe_scanner is None:
return web.json_response(
{"success": False, "error": "Recipe scanner unavailable"},
status=503,
)
data = await request.json()
recipe_ids = data.get("recipe_ids", [])
if not recipe_ids:
return web.json_response(
{"success": False, "error": "recipe_ids are required"},
status=400,
)
total = len(recipe_ids)
repaired = 0
skipped = 0
errors = 0
recipes = []
for recipe_id in recipe_ids:
try:
result = await recipe_scanner.repair_recipe_by_id(recipe_id)
if result.get("success"):
repaired += result.get("repaired", 0)
skipped += result.get("skipped", 0)
if result.get("recipe"):
recipes.append(result["recipe"])
else:
errors += 1
except RecipeNotFoundError:
skipped += 1
except Exception as exc:
self._logger.error(
"Error repairing recipe %s: %s", recipe_id, exc
)
errors += 1
return web.json_response({
"success": True,
"total": total,
"repaired": repaired,
"skipped": skipped,
"errors": errors,
"recipes": recipes,
})
except Exception as exc:
self._logger.error(
"Error performing bulk repair: %s", exc, exc_info=True
)
return web.json_response(
{"success": False, "error": str(exc)}, status=500
)
async def repair_recipe(self, request: web.Request) -> web.Response: async def repair_recipe(self, request: web.Request) -> web.Response:
try: try:
await self._ensure_dependencies_ready() await self._ensure_dependencies_ready()

View File

@@ -58,6 +58,7 @@ ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
RouteDefinition("POST", "/api/lm/recipes/repair", "repair_recipes"), RouteDefinition("POST", "/api/lm/recipes/repair", "repair_recipes"),
RouteDefinition("POST", "/api/lm/recipes/cancel-repair", "cancel_repair"), RouteDefinition("POST", "/api/lm/recipes/cancel-repair", "cancel_repair"),
RouteDefinition("POST", "/api/lm/recipe/{recipe_id}/repair", "repair_recipe"), RouteDefinition("POST", "/api/lm/recipe/{recipe_id}/repair", "repair_recipe"),
RouteDefinition("POST", "/api/lm/recipes/repair-bulk", "repair_recipes_bulk"),
RouteDefinition("GET", "/api/lm/recipes/repair-progress", "get_repair_progress"), RouteDefinition("GET", "/api/lm/recipes/repair-progress", "get_repair_progress"),
RouteDefinition("POST", "/api/lm/recipes/batch-import/start", "start_batch_import"), RouteDefinition("POST", "/api/lm/recipes/batch-import/start", "start_batch_import"),
RouteDefinition( RouteDefinition(

View File

@@ -65,7 +65,7 @@ class RecipeScanner:
cls._instance._civitai_client = None # Will be lazily initialized cls._instance._civitai_client = None # Will be lazily initialized
return cls._instance return cls._instance
REPAIR_VERSION = 3 REPAIR_VERSION = 4
def __init__( def __init__(
self, self,
@@ -292,6 +292,32 @@ class RecipeScanner:
if recipe.get("repair_version", 0) >= self.REPAIR_VERSION: if recipe.get("repair_version", 0) >= self.REPAIR_VERSION:
return False return False
# 1.5 Detect and clear corrupted checkpoint (LoRA data saved as checkpoint).
# A checkpoint whose modelVersionId also appears in a LoRA entry is
# definitely wrong — the CivitAI import code used to pick
# modelVersionIds[0] as the checkpoint, which was often a LoRA.
# Clearing it lets the enrichment flow re-resolve the correct
# checkpoint from CivitAI image metadata.
cp = recipe.get("checkpoint")
lora_mvids = {
l.get("modelVersionId")
for l in recipe.get("loras", [])
if l.get("modelVersionId")
}
if cp and cp.get("modelVersionId") and cp["modelVersionId"] in lora_mvids:
cp_mvid = cp["modelVersionId"]
logger.info(
"Recipe %s: checkpoint modelVersionId %s matches a LoRA — "
"clearing corrupted checkpoint and removing matching LoRA entry",
recipe.get("id"),
cp_mvid,
)
recipe["checkpoint"] = None
recipe["loras"] = [
l for l in recipe.get("loras", [])
if l.get("modelVersionId") != cp_mvid
]
# 2. Identification: Is repair needed? # 2. Identification: Is repair needed?
has_checkpoint = ( has_checkpoint = (
"checkpoint" in recipe "checkpoint" in recipe

View File

@@ -1,7 +1,7 @@
[project] [project]
name = "comfyui-lora-manager" name = "comfyui-lora-manager"
description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!" description = "Revolutionize your workflow with the ultimate LoRA companion for ComfyUI!"
version = "1.0.8" version = "1.0.9"
license = {file = "LICENSE"} license = {file = "LICENSE"}
dependencies = [ dependencies = [
"aiohttp", "aiohttp",

View File

@@ -15,6 +15,7 @@ const RECIPE_ENDPOINTS = {
move: '/api/lm/recipe/move', move: '/api/lm/recipe/move',
moveBulk: '/api/lm/recipes/move-bulk', moveBulk: '/api/lm/recipes/move-bulk',
bulkDelete: '/api/lm/recipes/bulk-delete', bulkDelete: '/api/lm/recipes/bulk-delete',
repairBulk: '/api/lm/recipes/repair-bulk',
}; };
const RECIPE_SIDEBAR_CONFIG = { const RECIPE_SIDEBAR_CONFIG = {
@@ -557,6 +558,38 @@ export class RecipeSidebarApiClient {
}; };
} }
async repairBulkModels(filePaths) {
if (!filePaths || filePaths.length === 0) {
throw new Error('No file paths provided');
}
const recipeIds = filePaths
.map((path) => extractRecipeId(path))
.filter((id) => !!id);
if (recipeIds.length === 0) {
throw new Error('No recipe IDs could be derived from file paths');
}
const response = await fetch(this.apiConfig.endpoints.repairBulk, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
recipe_ids: recipeIds,
}),
});
const result = await response.json();
if (!response.ok || !result.success) {
throw new Error(result.error || 'Failed to repair recipes');
}
return result;
}
async bulkDeleteModels(filePaths) { async bulkDeleteModels(filePaths) {
if (!filePaths || filePaths.length === 0) { if (!filePaths || filePaths.length === 0) {
throw new Error('No file paths provided'); throw new Error('No file paths provided');

View File

@@ -41,6 +41,11 @@ export class BulkContextMenu extends BaseContextMenu {
const autoOrganizeItem = this.menu.querySelector('[data-action="auto-organize"]'); const autoOrganizeItem = this.menu.querySelector('[data-action="auto-organize"]');
const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]'); const deleteAllItem = this.menu.querySelector('[data-action="delete-all"]');
const downloadMissingLorasItem = this.menu.querySelector('[data-action="download-missing-loras"]'); const downloadMissingLorasItem = this.menu.querySelector('[data-action="download-missing-loras"]');
const repairMetadataItem = this.menu.querySelector('[data-action="repair-metadata"]');
if (repairMetadataItem) {
repairMetadataItem.style.display = config.repairMetadata ? 'flex' : 'none';
}
if (sendToWorkflowAppendItem) { if (sendToWorkflowAppendItem) {
sendToWorkflowAppendItem.style.display = config.sendToWorkflow ? 'flex' : 'none'; sendToWorkflowAppendItem.style.display = config.sendToWorkflow ? 'flex' : 'none';
@@ -127,33 +132,38 @@ export class BulkContextMenu extends BaseContextMenu {
const resumeMetadataRefreshItem = this.menu.querySelector('[data-action="resume-metadata-refresh"]'); const resumeMetadataRefreshItem = this.menu.querySelector('[data-action="resume-metadata-refresh"]');
if (skipMetadataRefreshItem && resumeMetadataRefreshItem) { if (skipMetadataRefreshItem && resumeMetadataRefreshItem) {
const skipCount = this.countSkipStatus(true); if (!config.skipMetadataRefresh) {
const resumeCount = this.countSkipStatus(false);
const totalCount = skipCount + resumeCount;
if (skipCount === totalCount) {
skipMetadataRefreshItem.style.display = 'none'; skipMetadataRefreshItem.style.display = 'none';
resumeMetadataRefreshItem.style.display = 'flex';
resumeMetadataRefreshItem.querySelector('span').textContent = translate(
'loras.bulkOperations.resumeMetadataRefresh'
);
} else if (resumeCount === totalCount) {
skipMetadataRefreshItem.style.display = 'flex';
resumeMetadataRefreshItem.style.display = 'none'; resumeMetadataRefreshItem.style.display = 'none';
skipMetadataRefreshItem.querySelector('span').textContent = translate(
'loras.bulkOperations.skipMetadataRefresh'
);
} else { } else {
skipMetadataRefreshItem.style.display = 'flex'; const skipCount = this.countSkipStatus(true);
resumeMetadataRefreshItem.style.display = 'flex'; const resumeCount = this.countSkipStatus(false);
skipMetadataRefreshItem.querySelector('span').textContent = translate( const totalCount = skipCount + resumeCount;
'loras.bulkOperations.skipMetadataRefreshCount',
{ count: resumeCount } if (skipCount === totalCount) {
); skipMetadataRefreshItem.style.display = 'none';
resumeMetadataRefreshItem.querySelector('span').textContent = translate( resumeMetadataRefreshItem.style.display = 'flex';
'loras.bulkOperations.resumeMetadataRefreshCount', resumeMetadataRefreshItem.querySelector('span').textContent = translate(
{ count: skipCount } 'loras.bulkOperations.resumeMetadataRefresh'
); );
} else if (resumeCount === totalCount) {
skipMetadataRefreshItem.style.display = 'flex';
resumeMetadataRefreshItem.style.display = 'none';
skipMetadataRefreshItem.querySelector('span').textContent = translate(
'loras.bulkOperations.skipMetadataRefresh'
);
} else {
skipMetadataRefreshItem.style.display = 'flex';
resumeMetadataRefreshItem.style.display = 'flex';
skipMetadataRefreshItem.querySelector('span').textContent = translate(
'loras.bulkOperations.skipMetadataRefreshCount',
{ count: resumeCount }
);
resumeMetadataRefreshItem.querySelector('span').textContent = translate(
'loras.bulkOperations.resumeMetadataRefreshCount',
{ count: skipCount }
);
}
} }
} }
@@ -251,6 +261,9 @@ export class BulkContextMenu extends BaseContextMenu {
case 'delete-all': case 'delete-all':
bulkManager.showBulkDeleteModal(); bulkManager.showBulkDeleteModal();
break; break;
case 'repair-metadata':
bulkManager.repairSelectedRecipes();
break;
case 'set-favorite': { case 'set-favorite': {
const allFavorited = this.countFavoritedInSelection() === state.selectedModels.size; const allFavorited = this.countFavoritedInSelection() === state.selectedModels.size;
bulkManager.setBulkFavorites(!allFavorited); bulkManager.setBulkFavorites(!allFavorited);

View File

@@ -85,7 +85,8 @@ export class BulkManager {
setContentRating: false, setContentRating: false,
skipMetadataRefresh: false, skipMetadataRefresh: false,
setFavorite: true, setFavorite: true,
unfavorite: true unfavorite: true,
repairMetadata: true
} }
}; };
@@ -656,6 +657,76 @@ export class BulkManager {
} }
} }
async repairSelectedRecipes() {
if (state.selectedModels.size === 0) {
showToast('toast.recipes.noRecipesSelected', {}, 'warning');
return;
}
if (state.currentPageType !== 'recipes') {
showToast('This operation is only available for recipes', {}, 'warning');
return;
}
try {
const apiClient = this.getActiveApiClient();
const filePaths = Array.from(state.selectedModels);
if (typeof apiClient.repairBulkModels !== 'function') {
showToast('Bulk repair is not supported for this model type', {}, 'error');
return;
}
state.loadingManager.showSimpleLoading('Repairing recipe metadata...');
const result = await apiClient.repairBulkModels(filePaths);
if (result.success) {
const total = result.total || filePaths.length;
const repaired = result.repaired || 0;
const skipped = result.skipped || 0;
const recipes = result.recipes || [];
for (const recipe of recipes) {
if (recipe.file_path) {
state.virtualScroller.updateSingleItem(
recipe.file_path,
recipe
);
}
}
if (repaired > 0) {
showToast(
'toast.recipes.repairBulkComplete',
{ repaired, skipped, total },
'success'
);
} else {
showToast(
'toast.recipes.repairBulkSkipped',
{ total },
'info'
);
}
this.clearSelection();
} else {
throw new Error(result.error || 'Bulk repair failed');
}
} catch (error) {
console.error('Error during bulk recipe repair:', error);
showToast('toast.recipes.repairBulkFailed', { message: error.message }, 'error');
} finally {
if (state.loadingManager?.hide) {
state.loadingManager.hide();
}
if (typeof state.loadingManager?.restoreProgressBar === 'function') {
state.loadingManager.restoreProgressBar();
}
}
}
async refreshAllMetadata() { async refreshAllMetadata() {
if (state.selectedModels.size === 0) { if (state.selectedModels.size === 0) {
showToast('toast.models.noModelsSelected', {}, 'warning'); showToast('toast.models.noModelsSelected', {}, 'warning');

View File

@@ -80,6 +80,9 @@
<div class="context-menu-item" data-action="check-updates"> <div class="context-menu-item" data-action="check-updates">
<i class="fas fa-bell"></i> <span>{{ t('loras.bulkOperations.checkUpdates') }}</span> <i class="fas fa-bell"></i> <span>{{ t('loras.bulkOperations.checkUpdates') }}</span>
</div> </div>
<div class="context-menu-item" data-action="repair-metadata">
<i class="fas fa-tools"></i> <span>{{ t('loras.bulkOperations.repairMetadata') }}</span>
</div>
<div class="context-menu-item" data-action="skip-metadata-refresh"> <div class="context-menu-item" data-action="skip-metadata-refresh">
<i class="fas fa-ban"></i> <span>{{ t('loras.bulkOperations.skipMetadataRefresh') }}</span> <i class="fas fa-ban"></i> <span>{{ t('loras.bulkOperations.skipMetadataRefresh') }}</span>
</div> </div>