mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 22:52:12 -03:00
feat: Implement recipe repair cancellation with UI support and refactor LoadingManager to a singleton.
This commit is contained in:
@@ -167,6 +167,7 @@
|
|||||||
"label": "Recipe-Daten reparieren",
|
"label": "Recipe-Daten reparieren",
|
||||||
"loading": "Recipe-Daten werden repariert...",
|
"loading": "Recipe-Daten werden repariert...",
|
||||||
"success": "{count} Rezepte erfolgreich repariert.",
|
"success": "{count} Rezepte erfolgreich repariert.",
|
||||||
|
"cancelled": "Reparatur abgebrochen. {count} Rezepte wurden repariert.",
|
||||||
"error": "Recipe-Reparatur fehlgeschlagen: {message}"
|
"error": "Recipe-Reparatur fehlgeschlagen: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
"label": "Repair recipes data",
|
"label": "Repair recipes data",
|
||||||
"loading": "Repairing recipe data...",
|
"loading": "Repairing recipe data...",
|
||||||
"success": "Successfully repaired {count} recipes.",
|
"success": "Successfully repaired {count} recipes.",
|
||||||
|
"cancelled": "Repair cancelled. {count} recipes were repaired.",
|
||||||
"error": "Recipe repair failed: {message}"
|
"error": "Recipe repair failed: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
"label": "Reparar datos de recetas",
|
"label": "Reparar datos de recetas",
|
||||||
"loading": "Reparando datos de recetas...",
|
"loading": "Reparando datos de recetas...",
|
||||||
"success": "Se repararon con éxito {count} recetas.",
|
"success": "Se repararon con éxito {count} recetas.",
|
||||||
|
"cancelled": "Reparación cancelada. {count} recetas fueron reparadas.",
|
||||||
"error": "Error al reparar recetas: {message}"
|
"error": "Error al reparar recetas: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
"label": "Réparer les données de recettes",
|
"label": "Réparer les données de recettes",
|
||||||
"loading": "Réparation des données de recettes...",
|
"loading": "Réparation des données de recettes...",
|
||||||
"success": "{count} recettes réparées avec succès.",
|
"success": "{count} recettes réparées avec succès.",
|
||||||
|
"cancelled": "Réparation annulée. {count} recettes ont été réparées.",
|
||||||
"error": "Échec de la réparation des recettes : {message}"
|
"error": "Échec de la réparation des recettes : {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
"label": "תיקון נתוני מתכונים",
|
"label": "תיקון נתוני מתכונים",
|
||||||
"loading": "מתקן נתוני מתכונים...",
|
"loading": "מתקן נתוני מתכונים...",
|
||||||
"success": "תוקנו בהצלחה {count} מתכונים.",
|
"success": "תוקנו בהצלחה {count} מתכונים.",
|
||||||
|
"cancelled": "תיקון בוטל. {count} מתכונים תוקנו.",
|
||||||
"error": "תיקון המתכונים נכשל: {message}"
|
"error": "תיקון המתכונים נכשל: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
"label": "レシピデータの修復",
|
"label": "レシピデータの修復",
|
||||||
"loading": "レシピデータを修復中...",
|
"loading": "レシピデータを修復中...",
|
||||||
"success": "{count} 件のレシピを正常に修復しました。",
|
"success": "{count} 件のレシピを正常に修復しました。",
|
||||||
|
"cancelled": "修復がキャンセルされました。{count}個のレシピが修復されました。",
|
||||||
"error": "レシピの修復に失敗しました: {message}"
|
"error": "レシピの修復に失敗しました: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
"label": "레시피 데이터 복구",
|
"label": "레시피 데이터 복구",
|
||||||
"loading": "레시피 데이터 복구 중...",
|
"loading": "레시피 데이터 복구 중...",
|
||||||
"success": "{count}개의 레시피가 성공적으로 복구되었습니다.",
|
"success": "{count}개의 레시피가 성공적으로 복구되었습니다.",
|
||||||
|
"cancelled": "수리가 취소되었습니다. {count}개의 레시피가 수리되었습니다.",
|
||||||
"error": "레시피 복구 실패: {message}"
|
"error": "레시피 복구 실패: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
"label": "Восстановить данные рецептов",
|
"label": "Восстановить данные рецептов",
|
||||||
"loading": "Восстановление данных рецептов...",
|
"loading": "Восстановление данных рецептов...",
|
||||||
"success": "Успешно восстановлено {count} рецептов.",
|
"success": "Успешно восстановлено {count} рецептов.",
|
||||||
|
"cancelled": "Восстановление отменено. {count} рецептов было восстановлено.",
|
||||||
"error": "Ошибка восстановления рецептов: {message}"
|
"error": "Ошибка восстановления рецептов: {message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
"label": "修复配方数据",
|
"label": "修复配方数据",
|
||||||
"loading": "正在修复配方数据...",
|
"loading": "正在修复配方数据...",
|
||||||
"success": "成功修复了 {count} 个配方。",
|
"success": "成功修复了 {count} 个配方。",
|
||||||
|
"cancelled": "修复已取消。已修复 {count} 个配方。",
|
||||||
"error": "配方修复失败:{message}"
|
"error": "配方修复失败:{message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -167,6 +167,7 @@
|
|||||||
"label": "修復配方資料",
|
"label": "修復配方資料",
|
||||||
"loading": "正在修復配方資料...",
|
"loading": "正在修復配方資料...",
|
||||||
"success": "成功修復 {count} 個配方。",
|
"success": "成功修復 {count} 個配方。",
|
||||||
|
"cancelled": "修復已取消。已修復 {count} 個配方。",
|
||||||
"error": "配方修復失敗:{message}"
|
"error": "配方修復失敗:{message}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ class RecipeHandlerSet:
|
|||||||
"scan_recipes": self.query.scan_recipes,
|
"scan_recipes": self.query.scan_recipes,
|
||||||
"move_recipe": self.management.move_recipe,
|
"move_recipe": self.management.move_recipe,
|
||||||
"repair_recipes": self.management.repair_recipes,
|
"repair_recipes": self.management.repair_recipes,
|
||||||
|
"cancel_repair": self.management.cancel_repair,
|
||||||
"repair_recipe": self.management.repair_recipe,
|
"repair_recipe": self.management.repair_recipe,
|
||||||
"get_repair_progress": self.management.get_repair_progress,
|
"get_repair_progress": self.management.get_repair_progress,
|
||||||
}
|
}
|
||||||
@@ -530,9 +531,11 @@ class RecipeManagementHandler:
|
|||||||
return web.json_response({"success": False, "error": "Recipe scanner unavailable"}, status=503)
|
return web.json_response({"success": False, "error": "Recipe scanner unavailable"}, status=503)
|
||||||
|
|
||||||
# Check if already running
|
# Check if already running
|
||||||
if self._ws_manager.get_recipe_repair_progress():
|
if self._ws_manager.is_recipe_repair_running():
|
||||||
return web.json_response({"success": False, "error": "Recipe repair already in progress"}, status=409)
|
return web.json_response({"success": False, "error": "Recipe repair already in progress"}, status=409)
|
||||||
|
|
||||||
|
recipe_scanner.reset_cancellation()
|
||||||
|
|
||||||
async def progress_callback(data):
|
async def progress_callback(data):
|
||||||
await self._ws_manager.broadcast_recipe_repair_progress(data)
|
await self._ws_manager.broadcast_recipe_repair_progress(data)
|
||||||
|
|
||||||
@@ -551,6 +554,8 @@ class RecipeManagementHandler:
|
|||||||
finally:
|
finally:
|
||||||
# Keep the final status for a while so the UI can see it
|
# Keep the final status for a while so the UI can see it
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
# Don't cleanup if it was cancelled, let the UI see the cancelled state for a bit?
|
||||||
|
# Actually cleanup_recipe_repair_progress is fine as long as we waited enough.
|
||||||
self._ws_manager.cleanup_recipe_repair_progress()
|
self._ws_manager.cleanup_recipe_repair_progress()
|
||||||
|
|
||||||
asyncio.create_task(run_repair())
|
asyncio.create_task(run_repair())
|
||||||
@@ -560,6 +565,19 @@ class RecipeManagementHandler:
|
|||||||
self._logger.error("Error starting recipe repair: %s", exc, exc_info=True)
|
self._logger.error("Error starting 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 cancel_repair(self, request: web.Request) -> web.Response:
|
||||||
|
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)
|
||||||
|
|
||||||
|
recipe_scanner.cancel_task()
|
||||||
|
return web.json_response({"success": True, "message": "Cancellation requested"})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error("Error cancelling recipe 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()
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
|||||||
RouteDefinition("GET", "/api/lm/recipes/for-lora", "get_recipes_for_lora"),
|
RouteDefinition("GET", "/api/lm/recipes/for-lora", "get_recipes_for_lora"),
|
||||||
RouteDefinition("GET", "/api/lm/recipes/scan", "scan_recipes"),
|
RouteDefinition("GET", "/api/lm/recipes/scan", "scan_recipes"),
|
||||||
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/recipe/{recipe_id}/repair", "repair_recipe"),
|
RouteDefinition("POST", "/api/lm/recipe/{recipe_id}/repair", "repair_recipe"),
|
||||||
RouteDefinition("GET", "/api/lm/recipes/repair-progress", "get_repair_progress"),
|
RouteDefinition("GET", "/api/lm/recipes/repair-progress", "get_repair_progress"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ class RecipeScanner:
|
|||||||
self._mutation_lock = asyncio.Lock()
|
self._mutation_lock = asyncio.Lock()
|
||||||
self._post_scan_task: Optional[asyncio.Task] = None
|
self._post_scan_task: Optional[asyncio.Task] = None
|
||||||
self._resort_tasks: Set[asyncio.Task] = set()
|
self._resort_tasks: Set[asyncio.Task] = set()
|
||||||
|
self._cancel_requested = False
|
||||||
if lora_scanner:
|
if lora_scanner:
|
||||||
self._lora_scanner = lora_scanner
|
self._lora_scanner = lora_scanner
|
||||||
if checkpoint_scanner:
|
if checkpoint_scanner:
|
||||||
@@ -114,6 +115,19 @@ class RecipeScanner:
|
|||||||
self._civitai_client = await ServiceRegistry.get_civitai_client()
|
self._civitai_client = await ServiceRegistry.get_civitai_client()
|
||||||
return self._civitai_client
|
return self._civitai_client
|
||||||
|
|
||||||
|
def cancel_task(self) -> None:
|
||||||
|
"""Request cancellation of the current long-running task."""
|
||||||
|
self._cancel_requested = True
|
||||||
|
logger.info("Recipe Scanner: Cancellation requested")
|
||||||
|
|
||||||
|
def reset_cancellation(self) -> None:
|
||||||
|
"""Reset the cancellation flag."""
|
||||||
|
self._cancel_requested = False
|
||||||
|
|
||||||
|
def is_cancelled(self) -> bool:
|
||||||
|
"""Check if cancellation has been requested."""
|
||||||
|
return self._cancel_requested
|
||||||
|
|
||||||
async def repair_all_recipes(
|
async def repair_all_recipes(
|
||||||
self,
|
self,
|
||||||
progress_callback: Optional[Callable[[Dict], Any]] = None
|
progress_callback: Optional[Callable[[Dict], Any]] = None
|
||||||
@@ -127,6 +141,8 @@ class RecipeScanner:
|
|||||||
Returns:
|
Returns:
|
||||||
Dict summary of repair results
|
Dict summary of repair results
|
||||||
"""
|
"""
|
||||||
|
if progress_callback:
|
||||||
|
await progress_callback({"status": "started"})
|
||||||
async with self._mutation_lock:
|
async with self._mutation_lock:
|
||||||
cache = await self.get_cached_data()
|
cache = await self.get_cached_data()
|
||||||
all_recipes = list(cache.raw_data)
|
all_recipes = list(cache.raw_data)
|
||||||
@@ -136,8 +152,29 @@ class RecipeScanner:
|
|||||||
errors_count = 0
|
errors_count = 0
|
||||||
|
|
||||||
civitai_client = await self._get_civitai_client()
|
civitai_client = await self._get_civitai_client()
|
||||||
|
self.reset_cancellation()
|
||||||
|
|
||||||
for i, recipe in enumerate(all_recipes):
|
for i, recipe in enumerate(all_recipes):
|
||||||
|
if self.is_cancelled():
|
||||||
|
logger.info("Recipe repair cancelled by user")
|
||||||
|
if progress_callback:
|
||||||
|
await progress_callback({
|
||||||
|
"status": "cancelled",
|
||||||
|
"current": i,
|
||||||
|
"total": total,
|
||||||
|
"repaired": repaired_count,
|
||||||
|
"skipped": skipped_count,
|
||||||
|
"errors": errors_count
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"status": "cancelled",
|
||||||
|
"repaired": repaired_count,
|
||||||
|
"skipped": skipped_count,
|
||||||
|
"errors": errors_count,
|
||||||
|
"total": total
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Report progress
|
# Report progress
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
|
|||||||
@@ -212,8 +212,16 @@ class WebSocketManager:
|
|||||||
return self._recipe_repair_progress
|
return self._recipe_repair_progress
|
||||||
|
|
||||||
def cleanup_recipe_repair_progress(self):
|
def cleanup_recipe_repair_progress(self):
|
||||||
"""Clear recipe repair progress data"""
|
"""Clear recipe repair progress data if it is in a finished state"""
|
||||||
self._recipe_repair_progress = None
|
if self._recipe_repair_progress and self._recipe_repair_progress.get('status') in ['completed', 'cancelled', 'error']:
|
||||||
|
self._recipe_repair_progress = None
|
||||||
|
|
||||||
|
def is_recipe_repair_running(self) -> bool:
|
||||||
|
"""Check if recipe repair is currently running"""
|
||||||
|
if not self._recipe_repair_progress:
|
||||||
|
return False
|
||||||
|
status = self._recipe_repair_progress.get('status')
|
||||||
|
return status in ['started', 'processing']
|
||||||
|
|
||||||
def is_auto_organize_running(self) -> bool:
|
def is_auto_organize_running(self) -> bool:
|
||||||
"""Check if auto-organize is currently running"""
|
"""Check if auto-organize is currently running"""
|
||||||
|
|||||||
@@ -279,6 +279,7 @@ export class GlobalContextMenu extends BaseContextMenu {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const progressUI = state.loadingManager?.showEnhancedProgress(loadingMessage);
|
const progressUI = state.loadingManager?.showEnhancedProgress(loadingMessage);
|
||||||
|
progressUI?.showCancelButton(() => this.cancelRepair());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/lm/recipes/repair', {
|
const response = await fetch('/api/lm/recipes/repair', {
|
||||||
@@ -316,6 +317,14 @@ export class GlobalContextMenu extends BaseContextMenu {
|
|||||||
}
|
}
|
||||||
} else if (p.status === 'error') {
|
} else if (p.status === 'error') {
|
||||||
throw new Error(p.error || 'Repair failed');
|
throw new Error(p.error || 'Repair failed');
|
||||||
|
} else if (p.status === 'cancelled') {
|
||||||
|
isComplete = true;
|
||||||
|
progressUI?.complete(translate(
|
||||||
|
'globalContextMenu.repairRecipes.cancelled',
|
||||||
|
{ count: p.repaired },
|
||||||
|
`Repair cancelled. ${p.repaired} recipes were repaired.`
|
||||||
|
));
|
||||||
|
showToast('globalContextMenu.repairRecipes.cancelled', { count: p.repaired }, 'info');
|
||||||
}
|
}
|
||||||
} else if (progressResponse.status === 404) {
|
} else if (progressResponse.status === 404) {
|
||||||
// Progress might have finished quickly and been cleaned up
|
// Progress might have finished quickly and been cleaned up
|
||||||
@@ -337,4 +346,14 @@ export class GlobalContextMenu extends BaseContextMenu {
|
|||||||
menuItem?.classList.remove('disabled');
|
menuItem?.classList.remove('disabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancelRepair() {
|
||||||
|
try {
|
||||||
|
await fetch('/api/lm/recipes/cancel-repair', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to cancel recipe repair:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { formatFileSize } from '../utils/formatters.js';
|
|||||||
// Loading management
|
// Loading management
|
||||||
export class LoadingManager {
|
export class LoadingManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
if (LoadingManager.instance) {
|
||||||
|
return LoadingManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
this.overlay = document.getElementById('loading-overlay');
|
this.overlay = document.getElementById('loading-overlay');
|
||||||
|
|
||||||
if (!this.overlay) {
|
if (!this.overlay) {
|
||||||
@@ -59,6 +63,8 @@ export class LoadingManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.detailsContainer = null; // Will be created when needed
|
this.detailsContainer = null; // Will be created when needed
|
||||||
|
|
||||||
|
LoadingManager.instance = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
show(message = 'Loading...', progress = 0) {
|
show(message = 'Loading...', progress = 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user