feat: Implement recipe repair cancellation with UI support and refactor LoadingManager to a singleton.

This commit is contained in:
Will Miao
2026-01-02 20:03:27 +08:00
parent 837c32c42f
commit ab85ba54a9
16 changed files with 111 additions and 12 deletions

View File

@@ -167,6 +167,7 @@
"label": "Recipe-Daten reparieren",
"loading": "Recipe-Daten werden repariert...",
"success": "{count} Rezepte erfolgreich repariert.",
"cancelled": "Reparatur abgebrochen. {count} Rezepte wurden repariert.",
"error": "Recipe-Reparatur fehlgeschlagen: {message}"
}
},
@@ -1528,4 +1529,4 @@
"learnMore": "LM Civitai Extension Tutorial"
}
}
}
}

View File

@@ -167,6 +167,7 @@
"label": "Repair recipes data",
"loading": "Repairing recipe data...",
"success": "Successfully repaired {count} recipes.",
"cancelled": "Repair cancelled. {count} recipes were repaired.",
"error": "Recipe repair failed: {message}"
}
},

View File

@@ -167,6 +167,7 @@
"label": "Reparar datos de recetas",
"loading": "Reparando datos de recetas...",
"success": "Se repararon con éxito {count} recetas.",
"cancelled": "Reparación cancelada. {count} recetas fueron reparadas.",
"error": "Error al reparar recetas: {message}"
}
},
@@ -1528,4 +1529,4 @@
"learnMore": "LM Civitai Extension Tutorial"
}
}
}
}

View File

@@ -167,6 +167,7 @@
"label": "Réparer les données de recettes",
"loading": "Réparation des données de recettes...",
"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}"
}
},
@@ -1528,4 +1529,4 @@
"learnMore": "LM Civitai Extension Tutorial"
}
}
}
}

View File

@@ -167,6 +167,7 @@
"label": "תיקון נתוני מתכונים",
"loading": "מתקן נתוני מתכונים...",
"success": "תוקנו בהצלחה {count} מתכונים.",
"cancelled": "תיקון בוטל. {count} מתכונים תוקנו.",
"error": "תיקון המתכונים נכשל: {message}"
}
},
@@ -1528,4 +1529,4 @@
"learnMore": "LM Civitai Extension Tutorial"
}
}
}
}

View File

@@ -167,6 +167,7 @@
"label": "レシピデータの修復",
"loading": "レシピデータを修復中...",
"success": "{count} 件のレシピを正常に修復しました。",
"cancelled": "修復がキャンセルされました。{count}個のレシピが修復されました。",
"error": "レシピの修復に失敗しました: {message}"
}
},
@@ -1528,4 +1529,4 @@
"learnMore": "LM Civitai Extension Tutorial"
}
}
}
}

View File

@@ -167,6 +167,7 @@
"label": "레시피 데이터 복구",
"loading": "레시피 데이터 복구 중...",
"success": "{count}개의 레시피가 성공적으로 복구되었습니다.",
"cancelled": "수리가 취소되었습니다. {count}개의 레시피가 수리되었습니다.",
"error": "레시피 복구 실패: {message}"
}
},
@@ -1528,4 +1529,4 @@
"learnMore": "LM Civitai Extension Tutorial"
}
}
}
}

View File

@@ -167,6 +167,7 @@
"label": "Восстановить данные рецептов",
"loading": "Восстановление данных рецептов...",
"success": "Успешно восстановлено {count} рецептов.",
"cancelled": "Восстановление отменено. {count} рецептов было восстановлено.",
"error": "Ошибка восстановления рецептов: {message}"
}
},
@@ -1528,4 +1529,4 @@
"learnMore": "LM Civitai Extension Tutorial"
}
}
}
}

View File

@@ -167,6 +167,7 @@
"label": "修复配方数据",
"loading": "正在修复配方数据...",
"success": "成功修复了 {count} 个配方。",
"cancelled": "修复已取消。已修复 {count} 个配方。",
"error": "配方修复失败:{message}"
}
},
@@ -1528,4 +1529,4 @@
"learnMore": "浏览器插件教程"
}
}
}
}

View File

@@ -167,6 +167,7 @@
"label": "修復配方資料",
"loading": "正在修復配方資料...",
"success": "成功修復 {count} 個配方。",
"cancelled": "修復已取消。已修復 {count} 個配方。",
"error": "配方修復失敗:{message}"
}
},
@@ -1528,4 +1529,4 @@
"learnMore": "LM Civitai Extension Tutorial"
}
}
}
}

View File

@@ -78,6 +78,7 @@ class RecipeHandlerSet:
"scan_recipes": self.query.scan_recipes,
"move_recipe": self.management.move_recipe,
"repair_recipes": self.management.repair_recipes,
"cancel_repair": self.management.cancel_repair,
"repair_recipe": self.management.repair_recipe,
"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)
# 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)
recipe_scanner.reset_cancellation()
async def progress_callback(data):
await self._ws_manager.broadcast_recipe_repair_progress(data)
@@ -551,6 +554,8 @@ class RecipeManagementHandler:
finally:
# Keep the final status for a while so the UI can see it
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()
asyncio.create_task(run_repair())
@@ -560,6 +565,19 @@ class RecipeManagementHandler:
self._logger.error("Error starting recipe repair: %s", exc, exc_info=True)
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:
try:
await self._ensure_dependencies_ready()

View File

@@ -44,6 +44,7 @@ ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
RouteDefinition("GET", "/api/lm/recipes/for-lora", "get_recipes_for_lora"),
RouteDefinition("GET", "/api/lm/recipes/scan", "scan_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("GET", "/api/lm/recipes/repair-progress", "get_repair_progress"),
)

View File

@@ -73,6 +73,7 @@ class RecipeScanner:
self._mutation_lock = asyncio.Lock()
self._post_scan_task: Optional[asyncio.Task] = None
self._resort_tasks: Set[asyncio.Task] = set()
self._cancel_requested = False
if lora_scanner:
self._lora_scanner = lora_scanner
if checkpoint_scanner:
@@ -114,6 +115,19 @@ class RecipeScanner:
self._civitai_client = await ServiceRegistry.get_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(
self,
progress_callback: Optional[Callable[[Dict], Any]] = None
@@ -127,6 +141,8 @@ class RecipeScanner:
Returns:
Dict summary of repair results
"""
if progress_callback:
await progress_callback({"status": "started"})
async with self._mutation_lock:
cache = await self.get_cached_data()
all_recipes = list(cache.raw_data)
@@ -136,8 +152,29 @@ class RecipeScanner:
errors_count = 0
civitai_client = await self._get_civitai_client()
self.reset_cancellation()
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:
# Report progress
if progress_callback:

View File

@@ -212,8 +212,16 @@ class WebSocketManager:
return self._recipe_repair_progress
def cleanup_recipe_repair_progress(self):
"""Clear recipe repair progress data"""
self._recipe_repair_progress = None
"""Clear recipe repair progress data if it is in a finished state"""
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:
"""Check if auto-organize is currently running"""

View File

@@ -279,6 +279,7 @@ export class GlobalContextMenu extends BaseContextMenu {
);
const progressUI = state.loadingManager?.showEnhancedProgress(loadingMessage);
progressUI?.showCancelButton(() => this.cancelRepair());
try {
const response = await fetch('/api/lm/recipes/repair', {
@@ -316,6 +317,14 @@ export class GlobalContextMenu extends BaseContextMenu {
}
} else if (p.status === 'error') {
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) {
// Progress might have finished quickly and been cleaned up
@@ -337,4 +346,14 @@ export class GlobalContextMenu extends BaseContextMenu {
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);
}
}
}

View File

@@ -4,6 +4,10 @@ import { formatFileSize } from '../utils/formatters.js';
// Loading management
export class LoadingManager {
constructor() {
if (LoadingManager.instance) {
return LoadingManager.instance;
}
this.overlay = document.getElementById('loading-overlay');
if (!this.overlay) {
@@ -59,6 +63,8 @@ export class LoadingManager {
};
this.detailsContainer = null; // Will be created when needed
LoadingManager.instance = this;
}
show(message = 'Loading...', progress = 0) {