mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 07:05:43 -03:00
feat: implement task cancellation for model scanning and bulk operations
This commit is contained in:
@@ -1370,6 +1370,7 @@
|
|||||||
"verificationCompleteSuccess": "Verifikation abgeschlossen. Alle Dateien sind bestätigte Duplikate.",
|
"verificationCompleteSuccess": "Verifikation abgeschlossen. Alle Dateien sind bestätigte Duplikate.",
|
||||||
"verificationFailed": "Fehler beim Verifizieren der Hashes: {message}",
|
"verificationFailed": "Fehler beim Verifizieren der Hashes: {message}",
|
||||||
"noTagsToAdd": "Keine Tags zum Hinzufügen",
|
"noTagsToAdd": "Keine Tags zum Hinzufügen",
|
||||||
|
"bulkTagsUpdating": "Tags für {count} Modell(e) werden aktualisiert...",
|
||||||
"tagsAddedSuccessfully": "Erfolgreich {tagCount} Tag(s) zu {count} {type}(s) hinzugefügt",
|
"tagsAddedSuccessfully": "Erfolgreich {tagCount} Tag(s) zu {count} {type}(s) hinzugefügt",
|
||||||
"tagsReplacedSuccessfully": "Tags für {count} {type}(s) erfolgreich durch {tagCount} Tag(s) ersetzt",
|
"tagsReplacedSuccessfully": "Tags für {count} {type}(s) erfolgreich durch {tagCount} Tag(s) ersetzt",
|
||||||
"tagsAddFailed": "Fehler beim Hinzufügen von Tags zu {count} Modell(en)",
|
"tagsAddFailed": "Fehler beim Hinzufügen von Tags zu {count} Modell(en)",
|
||||||
@@ -1490,6 +1491,8 @@
|
|||||||
"metadataRefreshed": "Metadaten erfolgreich aktualisiert",
|
"metadataRefreshed": "Metadaten erfolgreich aktualisiert",
|
||||||
"metadataRefreshFailed": "Fehler beim Aktualisieren der Metadaten: {message}",
|
"metadataRefreshFailed": "Fehler beim Aktualisieren der Metadaten: {message}",
|
||||||
"metadataUpdateComplete": "Metadaten-Update abgeschlossen",
|
"metadataUpdateComplete": "Metadaten-Update abgeschlossen",
|
||||||
|
"operationCancelled": "Vorgang vom Benutzer abgebrochen",
|
||||||
|
"operationCancelledPartial": "Vorgang abgebrochen. {success} Elemente verarbeitet.",
|
||||||
"metadataFetchFailed": "Fehler beim Abrufen der Metadaten: {message}",
|
"metadataFetchFailed": "Fehler beim Abrufen der Metadaten: {message}",
|
||||||
"bulkMetadataCompleteAll": "Alle {count} {type}s erfolgreich aktualisiert",
|
"bulkMetadataCompleteAll": "Alle {count} {type}s erfolgreich aktualisiert",
|
||||||
"bulkMetadataCompletePartial": "{success} von {total} {type}s aktualisiert",
|
"bulkMetadataCompletePartial": "{success} von {total} {type}s aktualisiert",
|
||||||
|
|||||||
@@ -1370,6 +1370,7 @@
|
|||||||
"verificationCompleteSuccess": "Verification complete. All files are confirmed duplicates.",
|
"verificationCompleteSuccess": "Verification complete. All files are confirmed duplicates.",
|
||||||
"verificationFailed": "Failed to verify hashes: {message}",
|
"verificationFailed": "Failed to verify hashes: {message}",
|
||||||
"noTagsToAdd": "No tags to add",
|
"noTagsToAdd": "No tags to add",
|
||||||
|
"bulkTagsUpdating": "Updating tags for {count} model(s)...",
|
||||||
"tagsAddedSuccessfully": "Successfully added {tagCount} tag(s) to {count} {type}(s)",
|
"tagsAddedSuccessfully": "Successfully added {tagCount} tag(s) to {count} {type}(s)",
|
||||||
"tagsReplacedSuccessfully": "Successfully replaced tags for {count} {type}(s) with {tagCount} tag(s)",
|
"tagsReplacedSuccessfully": "Successfully replaced tags for {count} {type}(s) with {tagCount} tag(s)",
|
||||||
"tagsAddFailed": "Failed to add tags to {count} model(s)",
|
"tagsAddFailed": "Failed to add tags to {count} model(s)",
|
||||||
@@ -1490,6 +1491,8 @@
|
|||||||
"metadataRefreshed": "Metadata refreshed successfully",
|
"metadataRefreshed": "Metadata refreshed successfully",
|
||||||
"metadataRefreshFailed": "Failed to refresh metadata: {message}",
|
"metadataRefreshFailed": "Failed to refresh metadata: {message}",
|
||||||
"metadataUpdateComplete": "Metadata update complete",
|
"metadataUpdateComplete": "Metadata update complete",
|
||||||
|
"operationCancelled": "Operation cancelled by user",
|
||||||
|
"operationCancelledPartial": "Operation cancelled. {success} items processed.",
|
||||||
"metadataFetchFailed": "Failed to fetch metadata: {message}",
|
"metadataFetchFailed": "Failed to fetch metadata: {message}",
|
||||||
"bulkMetadataCompleteAll": "Successfully refreshed all {count} {type}s",
|
"bulkMetadataCompleteAll": "Successfully refreshed all {count} {type}s",
|
||||||
"bulkMetadataCompletePartial": "Refreshed {success} of {total} {type}s",
|
"bulkMetadataCompletePartial": "Refreshed {success} of {total} {type}s",
|
||||||
|
|||||||
@@ -1370,6 +1370,7 @@
|
|||||||
"verificationCompleteSuccess": "Verificación completa. Todos los archivos son confirmados duplicados.",
|
"verificationCompleteSuccess": "Verificación completa. Todos los archivos son confirmados duplicados.",
|
||||||
"verificationFailed": "Error al verificar hashes: {message}",
|
"verificationFailed": "Error al verificar hashes: {message}",
|
||||||
"noTagsToAdd": "No hay etiquetas para añadir",
|
"noTagsToAdd": "No hay etiquetas para añadir",
|
||||||
|
"bulkTagsUpdating": "Actualizando etiquetas para {count} modelo(s)...",
|
||||||
"tagsAddedSuccessfully": "Se añadieron exitosamente {tagCount} etiqueta(s) a {count} {type}(s)",
|
"tagsAddedSuccessfully": "Se añadieron exitosamente {tagCount} etiqueta(s) a {count} {type}(s)",
|
||||||
"tagsReplacedSuccessfully": "Se reemplazaron exitosamente las etiquetas de {count} {type}(s) con {tagCount} etiqueta(s)",
|
"tagsReplacedSuccessfully": "Se reemplazaron exitosamente las etiquetas de {count} {type}(s) con {tagCount} etiqueta(s)",
|
||||||
"tagsAddFailed": "Error al añadir etiquetas a {count} modelo(s)",
|
"tagsAddFailed": "Error al añadir etiquetas a {count} modelo(s)",
|
||||||
@@ -1490,6 +1491,8 @@
|
|||||||
"metadataRefreshed": "Metadatos actualizados exitosamente",
|
"metadataRefreshed": "Metadatos actualizados exitosamente",
|
||||||
"metadataRefreshFailed": "Error al actualizar metadatos: {message}",
|
"metadataRefreshFailed": "Error al actualizar metadatos: {message}",
|
||||||
"metadataUpdateComplete": "Actualización de metadatos completada",
|
"metadataUpdateComplete": "Actualización de metadatos completada",
|
||||||
|
"operationCancelled": "Operación cancelada por el usuario",
|
||||||
|
"operationCancelledPartial": "Operación cancelada. {success} elementos procesados.",
|
||||||
"metadataFetchFailed": "Error al obtener metadatos: {message}",
|
"metadataFetchFailed": "Error al obtener metadatos: {message}",
|
||||||
"bulkMetadataCompleteAll": "Actualizados exitosamente todos los {count} {type}s",
|
"bulkMetadataCompleteAll": "Actualizados exitosamente todos los {count} {type}s",
|
||||||
"bulkMetadataCompletePartial": "Actualizados {success} de {total} {type}s",
|
"bulkMetadataCompletePartial": "Actualizados {success} de {total} {type}s",
|
||||||
|
|||||||
@@ -1370,6 +1370,7 @@
|
|||||||
"verificationCompleteSuccess": "Vérification terminée. Tous les fichiers sont confirmés comme doublons.",
|
"verificationCompleteSuccess": "Vérification terminée. Tous les fichiers sont confirmés comme doublons.",
|
||||||
"verificationFailed": "Échec de la vérification des hash : {message}",
|
"verificationFailed": "Échec de la vérification des hash : {message}",
|
||||||
"noTagsToAdd": "Aucun tag à ajouter",
|
"noTagsToAdd": "Aucun tag à ajouter",
|
||||||
|
"bulkTagsUpdating": "Mise à jour des tags pour {count} modèle(s)...",
|
||||||
"tagsAddedSuccessfully": "{tagCount} tag(s) ajouté(s) avec succès à {count} {type}(s)",
|
"tagsAddedSuccessfully": "{tagCount} tag(s) ajouté(s) avec succès à {count} {type}(s)",
|
||||||
"tagsReplacedSuccessfully": "Tags remplacés avec succès pour {count} {type}(s) avec {tagCount} tag(s)",
|
"tagsReplacedSuccessfully": "Tags remplacés avec succès pour {count} {type}(s) avec {tagCount} tag(s)",
|
||||||
"tagsAddFailed": "Échec de l'ajout des tags à {count} modèle(s)",
|
"tagsAddFailed": "Échec de l'ajout des tags à {count} modèle(s)",
|
||||||
@@ -1490,6 +1491,8 @@
|
|||||||
"metadataRefreshed": "Métadonnées actualisées avec succès",
|
"metadataRefreshed": "Métadonnées actualisées avec succès",
|
||||||
"metadataRefreshFailed": "Échec de l'actualisation des métadonnées : {message}",
|
"metadataRefreshFailed": "Échec de l'actualisation des métadonnées : {message}",
|
||||||
"metadataUpdateComplete": "Mise à jour des métadonnées terminée",
|
"metadataUpdateComplete": "Mise à jour des métadonnées terminée",
|
||||||
|
"operationCancelled": "Opération annulée par l'utilisateur",
|
||||||
|
"operationCancelledPartial": "Opération annulée. {success} éléments traités.",
|
||||||
"metadataFetchFailed": "Échec de la récupération des métadonnées : {message}",
|
"metadataFetchFailed": "Échec de la récupération des métadonnées : {message}",
|
||||||
"bulkMetadataCompleteAll": "Actualisation réussie de tous les {count} {type}s",
|
"bulkMetadataCompleteAll": "Actualisation réussie de tous les {count} {type}s",
|
||||||
"bulkMetadataCompletePartial": "{success} sur {total} {type}s actualisés",
|
"bulkMetadataCompletePartial": "{success} sur {total} {type}s actualisés",
|
||||||
|
|||||||
@@ -1370,6 +1370,7 @@
|
|||||||
"verificationCompleteSuccess": "האימות הושלם. כל הקבצים אושרו ככפולים.",
|
"verificationCompleteSuccess": "האימות הושלם. כל הקבצים אושרו ככפולים.",
|
||||||
"verificationFailed": "אימות ה-hashes נכשל: {message}",
|
"verificationFailed": "אימות ה-hashes נכשל: {message}",
|
||||||
"noTagsToAdd": "אין תגיות להוספה",
|
"noTagsToAdd": "אין תגיות להוספה",
|
||||||
|
"bulkTagsUpdating": "מעדכן תגיות עבור {count} מודלים...",
|
||||||
"tagsAddedSuccessfully": "נוספו בהצלחה {tagCount} תגית(ות) ל-{count} {type}(ים)",
|
"tagsAddedSuccessfully": "נוספו בהצלחה {tagCount} תגית(ות) ל-{count} {type}(ים)",
|
||||||
"tagsReplacedSuccessfully": "הוחלפו בהצלחה תגיות עבור {count} {type}(ים) ב-{tagCount} תגית(ות)",
|
"tagsReplacedSuccessfully": "הוחלפו בהצלחה תגיות עבור {count} {type}(ים) ב-{tagCount} תגית(ות)",
|
||||||
"tagsAddFailed": "הוספת תגיות ל-{count} מודל(ים) נכשלה",
|
"tagsAddFailed": "הוספת תגיות ל-{count} מודל(ים) נכשלה",
|
||||||
@@ -1490,6 +1491,8 @@
|
|||||||
"metadataRefreshed": "המטא-דאטה רועננה בהצלחה",
|
"metadataRefreshed": "המטא-דאטה רועננה בהצלחה",
|
||||||
"metadataRefreshFailed": "רענון המטא-דאטה נכשל: {message}",
|
"metadataRefreshFailed": "רענון המטא-דאטה נכשל: {message}",
|
||||||
"metadataUpdateComplete": "עדכון המטא-דאטה הושלם",
|
"metadataUpdateComplete": "עדכון המטא-דאטה הושלם",
|
||||||
|
"operationCancelled": "הפעולה בוטלה על ידי המשתמש",
|
||||||
|
"operationCancelledPartial": "הפעולה בוטלה. {success} פריטים עובדו.",
|
||||||
"metadataFetchFailed": "אחזור המטא-דאטה נכשל: {message}",
|
"metadataFetchFailed": "אחזור המטא-דאטה נכשל: {message}",
|
||||||
"bulkMetadataCompleteAll": "רועננו בהצלחה כל {count} ה-{type}s",
|
"bulkMetadataCompleteAll": "רועננו בהצלחה כל {count} ה-{type}s",
|
||||||
"bulkMetadataCompletePartial": "רועננו {success} מתוך {total} {type}s",
|
"bulkMetadataCompletePartial": "רועננו {success} מתוך {total} {type}s",
|
||||||
|
|||||||
@@ -1370,6 +1370,7 @@
|
|||||||
"verificationCompleteSuccess": "検証完了。すべてのファイルが重複であることが確認されました。",
|
"verificationCompleteSuccess": "検証完了。すべてのファイルが重複であることが確認されました。",
|
||||||
"verificationFailed": "ハッシュの検証に失敗しました:{message}",
|
"verificationFailed": "ハッシュの検証に失敗しました:{message}",
|
||||||
"noTagsToAdd": "追加するタグがありません",
|
"noTagsToAdd": "追加するタグがありません",
|
||||||
|
"bulkTagsUpdating": "{count} 個のモデルのタグを更新しています...",
|
||||||
"tagsAddedSuccessfully": "{count} {type} に {tagCount} 個のタグを追加しました",
|
"tagsAddedSuccessfully": "{count} {type} に {tagCount} 個のタグを追加しました",
|
||||||
"tagsReplacedSuccessfully": "{count} {type} のタグを {tagCount} 個に置換しました",
|
"tagsReplacedSuccessfully": "{count} {type} のタグを {tagCount} 個に置換しました",
|
||||||
"tagsAddFailed": "{count} モデルへのタグ追加に失敗しました",
|
"tagsAddFailed": "{count} モデルへのタグ追加に失敗しました",
|
||||||
@@ -1490,6 +1491,8 @@
|
|||||||
"metadataRefreshed": "メタデータが正常に更新されました",
|
"metadataRefreshed": "メタデータが正常に更新されました",
|
||||||
"metadataRefreshFailed": "メタデータの更新に失敗しました:{message}",
|
"metadataRefreshFailed": "メタデータの更新に失敗しました:{message}",
|
||||||
"metadataUpdateComplete": "メタデータ更新完了",
|
"metadataUpdateComplete": "メタデータ更新完了",
|
||||||
|
"operationCancelled": "ユーザーによって操作がキャンセルされました",
|
||||||
|
"operationCancelledPartial": "操作がキャンセルされました。{success} 個の項目が処理されました。",
|
||||||
"metadataFetchFailed": "メタデータの取得に失敗しました:{message}",
|
"metadataFetchFailed": "メタデータの取得に失敗しました:{message}",
|
||||||
"bulkMetadataCompleteAll": "{count} {type}すべてが正常に更新されました",
|
"bulkMetadataCompleteAll": "{count} {type}すべてが正常に更新されました",
|
||||||
"bulkMetadataCompletePartial": "{total} {type}のうち {success} が更新されました",
|
"bulkMetadataCompletePartial": "{total} {type}のうち {success} が更新されました",
|
||||||
|
|||||||
@@ -1370,6 +1370,7 @@
|
|||||||
"verificationCompleteSuccess": "검증 완료. 모든 파일이 중복임을 확인했습니다.",
|
"verificationCompleteSuccess": "검증 완료. 모든 파일이 중복임을 확인했습니다.",
|
||||||
"verificationFailed": "해시 검증 실패: {message}",
|
"verificationFailed": "해시 검증 실패: {message}",
|
||||||
"noTagsToAdd": "추가할 태그가 없습니다",
|
"noTagsToAdd": "추가할 태그가 없습니다",
|
||||||
|
"bulkTagsUpdating": "{count}개 모델의 태그를 업데이트 중입니다...",
|
||||||
"tagsAddedSuccessfully": "{count}개의 {type}에 {tagCount}개의 태그가 성공적으로 추가되었습니다",
|
"tagsAddedSuccessfully": "{count}개의 {type}에 {tagCount}개의 태그가 성공적으로 추가되었습니다",
|
||||||
"tagsReplacedSuccessfully": "{count}개의 {type}의 태그가 {tagCount}개의 태그로 성공적으로 교체되었습니다",
|
"tagsReplacedSuccessfully": "{count}개의 {type}의 태그가 {tagCount}개의 태그로 성공적으로 교체되었습니다",
|
||||||
"tagsAddFailed": "{count}개의 모델에 태그 추가에 실패했습니다",
|
"tagsAddFailed": "{count}개의 모델에 태그 추가에 실패했습니다",
|
||||||
@@ -1490,6 +1491,8 @@
|
|||||||
"metadataRefreshed": "메타데이터가 성공적으로 새로고침되었습니다",
|
"metadataRefreshed": "메타데이터가 성공적으로 새로고침되었습니다",
|
||||||
"metadataRefreshFailed": "메타데이터 새로고침 실패: {message}",
|
"metadataRefreshFailed": "메타데이터 새로고침 실패: {message}",
|
||||||
"metadataUpdateComplete": "메타데이터 업데이트 완료",
|
"metadataUpdateComplete": "메타데이터 업데이트 완료",
|
||||||
|
"operationCancelled": "사용자에 의해 작업이 취소되었습니다",
|
||||||
|
"operationCancelledPartial": "작업이 취소되었습니다. {success}개 항목이 처리되었습니다.",
|
||||||
"metadataFetchFailed": "메타데이터 가져오기 실패: {message}",
|
"metadataFetchFailed": "메타데이터 가져오기 실패: {message}",
|
||||||
"bulkMetadataCompleteAll": "모든 {count}개 {type}이(가) 성공적으로 새로고침되었습니다",
|
"bulkMetadataCompleteAll": "모든 {count}개 {type}이(가) 성공적으로 새로고침되었습니다",
|
||||||
"bulkMetadataCompletePartial": "{total}개 중 {success}개 {type}이(가) 새로고침되었습니다",
|
"bulkMetadataCompletePartial": "{total}개 중 {success}개 {type}이(가) 새로고침되었습니다",
|
||||||
|
|||||||
@@ -1370,6 +1370,7 @@
|
|||||||
"verificationCompleteSuccess": "Проверка завершена. Все файлы подтверждены как дубликаты.",
|
"verificationCompleteSuccess": "Проверка завершена. Все файлы подтверждены как дубликаты.",
|
||||||
"verificationFailed": "Не удалось проверить хеши: {message}",
|
"verificationFailed": "Не удалось проверить хеши: {message}",
|
||||||
"noTagsToAdd": "Нет тегов для добавления",
|
"noTagsToAdd": "Нет тегов для добавления",
|
||||||
|
"bulkTagsUpdating": "Обновление тегов для {count} модел(ей)...",
|
||||||
"tagsAddedSuccessfully": "Успешно добавлено {tagCount} тег(ов) к {count} {type}(ам)",
|
"tagsAddedSuccessfully": "Успешно добавлено {tagCount} тег(ов) к {count} {type}(ам)",
|
||||||
"tagsReplacedSuccessfully": "Успешно заменены теги для {count} {type}(ов) на {tagCount} тег(ов)",
|
"tagsReplacedSuccessfully": "Успешно заменены теги для {count} {type}(ов) на {tagCount} тег(ов)",
|
||||||
"tagsAddFailed": "Не удалось добавить теги к {count} модель(ям)",
|
"tagsAddFailed": "Не удалось добавить теги к {count} модель(ям)",
|
||||||
@@ -1490,6 +1491,8 @@
|
|||||||
"metadataRefreshed": "Метаданные успешно обновлены",
|
"metadataRefreshed": "Метаданные успешно обновлены",
|
||||||
"metadataRefreshFailed": "Не удалось обновить метаданные: {message}",
|
"metadataRefreshFailed": "Не удалось обновить метаданные: {message}",
|
||||||
"metadataUpdateComplete": "Обновление метаданных завершено",
|
"metadataUpdateComplete": "Обновление метаданных завершено",
|
||||||
|
"operationCancelled": "Операция отменена пользователем",
|
||||||
|
"operationCancelledPartial": "Операция отменена. Обработано {success} элементов.",
|
||||||
"metadataFetchFailed": "Не удалось получить метаданные: {message}",
|
"metadataFetchFailed": "Не удалось получить метаданные: {message}",
|
||||||
"bulkMetadataCompleteAll": "Успешно обновлены все {count} {type}s",
|
"bulkMetadataCompleteAll": "Успешно обновлены все {count} {type}s",
|
||||||
"bulkMetadataCompletePartial": "Обновлено {success} из {total} {type}s",
|
"bulkMetadataCompletePartial": "Обновлено {success} из {total} {type}s",
|
||||||
|
|||||||
@@ -1370,6 +1370,7 @@
|
|||||||
"verificationCompleteSuccess": "验证完成。所有文件均为重复项。",
|
"verificationCompleteSuccess": "验证完成。所有文件均为重复项。",
|
||||||
"verificationFailed": "验证哈希失败:{message}",
|
"verificationFailed": "验证哈希失败:{message}",
|
||||||
"noTagsToAdd": "没有可添加的标签",
|
"noTagsToAdd": "没有可添加的标签",
|
||||||
|
"bulkTagsUpdating": "正在更新 {count} 个模型的标签...",
|
||||||
"tagsAddedSuccessfully": "已成功为 {count} 个 {type} 添加 {tagCount} 个标签",
|
"tagsAddedSuccessfully": "已成功为 {count} 个 {type} 添加 {tagCount} 个标签",
|
||||||
"tagsReplacedSuccessfully": "已成功为 {count} 个 {type} 替换为 {tagCount} 个标签",
|
"tagsReplacedSuccessfully": "已成功为 {count} 个 {type} 替换为 {tagCount} 个标签",
|
||||||
"tagsAddFailed": "为 {count} 个模型添加标签失败",
|
"tagsAddFailed": "为 {count} 个模型添加标签失败",
|
||||||
@@ -1490,6 +1491,8 @@
|
|||||||
"metadataRefreshed": "元数据刷新成功",
|
"metadataRefreshed": "元数据刷新成功",
|
||||||
"metadataRefreshFailed": "刷新元数据失败:{message}",
|
"metadataRefreshFailed": "刷新元数据失败:{message}",
|
||||||
"metadataUpdateComplete": "元数据更新完成",
|
"metadataUpdateComplete": "元数据更新完成",
|
||||||
|
"operationCancelled": "操作已由用户取消",
|
||||||
|
"operationCancelledPartial": "操作已取消。已处理 {success} 个项目。",
|
||||||
"metadataFetchFailed": "获取元数据失败:{message}",
|
"metadataFetchFailed": "获取元数据失败:{message}",
|
||||||
"bulkMetadataCompleteAll": "全部 {count} 个 {type} 元数据刷新成功",
|
"bulkMetadataCompleteAll": "全部 {count} 个 {type} 元数据刷新成功",
|
||||||
"bulkMetadataCompletePartial": "已刷新 {success}/{total} 个 {type} 元数据",
|
"bulkMetadataCompletePartial": "已刷新 {success}/{total} 个 {type} 元数据",
|
||||||
|
|||||||
@@ -1370,6 +1370,7 @@
|
|||||||
"verificationCompleteSuccess": "驗證完成。所有檔案均確認為重複項。",
|
"verificationCompleteSuccess": "驗證完成。所有檔案均確認為重複項。",
|
||||||
"verificationFailed": "驗證雜湊失敗:{message}",
|
"verificationFailed": "驗證雜湊失敗:{message}",
|
||||||
"noTagsToAdd": "沒有可新增的標籤",
|
"noTagsToAdd": "沒有可新增的標籤",
|
||||||
|
"bulkTagsUpdating": "正在更新 {count} 個模型的標籤...",
|
||||||
"tagsAddedSuccessfully": "已成功將 {tagCount} 個標籤新增到 {count} 個 {type}",
|
"tagsAddedSuccessfully": "已成功將 {tagCount} 個標籤新增到 {count} 個 {type}",
|
||||||
"tagsReplacedSuccessfully": "已成功以 {tagCount} 個標籤取代 {count} 個 {type} 的標籤",
|
"tagsReplacedSuccessfully": "已成功以 {tagCount} 個標籤取代 {count} 個 {type} 的標籤",
|
||||||
"tagsAddFailed": "新增標籤到 {count} 個模型失敗",
|
"tagsAddFailed": "新增標籤到 {count} 個模型失敗",
|
||||||
@@ -1490,6 +1491,8 @@
|
|||||||
"metadataRefreshed": "metadata 已成功刷新",
|
"metadataRefreshed": "metadata 已成功刷新",
|
||||||
"metadataRefreshFailed": "刷新 metadata 失敗:{message}",
|
"metadataRefreshFailed": "刷新 metadata 失敗:{message}",
|
||||||
"metadataUpdateComplete": "metadata 更新完成",
|
"metadataUpdateComplete": "metadata 更新完成",
|
||||||
|
"operationCancelled": "操作已由用戶取消",
|
||||||
|
"operationCancelledPartial": "操作已取消。已處理 {success} 個項目。",
|
||||||
"metadataFetchFailed": "取得 metadata 失敗:{message}",
|
"metadataFetchFailed": "取得 metadata 失敗:{message}",
|
||||||
"bulkMetadataCompleteAll": "已成功刷新全部 {count} 個 {type}",
|
"bulkMetadataCompleteAll": "已成功刷新全部 {count} 個 {type}",
|
||||||
"bulkMetadataCompletePartial": "已刷新 {success} / {total} 個 {type}",
|
"bulkMetadataCompletePartial": "已刷新 {success} / {total} 個 {type}",
|
||||||
|
|||||||
@@ -618,6 +618,8 @@ class ModelQueryHandler:
|
|||||||
try:
|
try:
|
||||||
full_rebuild = request.query.get("full_rebuild", "false").lower() == "true"
|
full_rebuild = request.query.get("full_rebuild", "false").lower() == "true"
|
||||||
await self._service.scan_models(force_refresh=True, rebuild_cache=full_rebuild)
|
await self._service.scan_models(force_refresh=True, rebuild_cache=full_rebuild)
|
||||||
|
if self._service.scanner.is_cancelled():
|
||||||
|
return web.json_response({"status": "cancelled", "message": f"{self._service.model_type.capitalize()} scan cancelled"})
|
||||||
return web.json_response({"status": "success", "message": f"{self._service.model_type.capitalize()} scan completed"})
|
return web.json_response({"status": "success", "message": f"{self._service.model_type.capitalize()} scan completed"})
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._logger.error("Error scanning %ss: %s", self._service.model_type, exc, exc_info=True)
|
self._logger.error("Error scanning %ss: %s", self._service.model_type, exc, exc_info=True)
|
||||||
@@ -639,6 +641,14 @@ class ModelQueryHandler:
|
|||||||
self._logger.error("Error getting folders: %s", exc)
|
self._logger.error("Error getting folders: %s", exc)
|
||||||
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_task(self, request: web.Request) -> web.Response:
|
||||||
|
try:
|
||||||
|
self._service.scanner.cancel_task()
|
||||||
|
return web.json_response({"status": "success", "message": "Cancellation requested"})
|
||||||
|
except Exception as exc:
|
||||||
|
self._logger.error("Error cancelling task for %s: %s", self._service.model_type, exc)
|
||||||
|
return web.json_response({"success": False, "error": str(exc)}, status=500)
|
||||||
|
|
||||||
async def get_folder_tree(self, request: web.Request) -> web.Response:
|
async def get_folder_tree(self, request: web.Request) -> web.Response:
|
||||||
try:
|
try:
|
||||||
model_root = request.query.get("model_root")
|
model_root = request.query.get("model_root")
|
||||||
@@ -1262,6 +1272,8 @@ class ModelUpdateHandler:
|
|||||||
force_refresh=force_refresh,
|
force_refresh=force_refresh,
|
||||||
target_model_ids=target_model_ids or None,
|
target_model_ids=target_model_ids or None,
|
||||||
)
|
)
|
||||||
|
if self._service.scanner.is_cancelled():
|
||||||
|
return web.json_response({"success": False, "status": "cancelled", "message": "Update refresh cancelled"})
|
||||||
except RateLimitError as exc:
|
except RateLimitError as exc:
|
||||||
return web.json_response(
|
return web.json_response(
|
||||||
{"success": False, "error": str(exc) or "Rate limited"}, status=429
|
{"success": False, "error": str(exc) or "Rate limited"}, status=429
|
||||||
@@ -1678,4 +1690,5 @@ class ModelHandlerSet:
|
|||||||
"set_version_update_ignore": self.updates.set_version_update_ignore,
|
"set_version_update_ignore": self.updates.set_version_update_ignore,
|
||||||
"get_model_update_status": self.updates.get_model_update_status,
|
"get_model_update_status": self.updates.get_model_update_status,
|
||||||
"get_model_versions": self.updates.get_model_versions,
|
"get_model_versions": self.updates.get_model_versions,
|
||||||
|
"cancel_task": self.query.cancel_task,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ COMMON_ROUTE_DEFINITIONS: tuple[RouteDefinition, ...] = (
|
|||||||
RouteDefinition("GET", "/api/lm/pause-download", "pause_download_get"),
|
RouteDefinition("GET", "/api/lm/pause-download", "pause_download_get"),
|
||||||
RouteDefinition("GET", "/api/lm/resume-download", "resume_download_get"),
|
RouteDefinition("GET", "/api/lm/resume-download", "resume_download_get"),
|
||||||
RouteDefinition("GET", "/api/lm/download-progress/{download_id}", "get_download_progress"),
|
RouteDefinition("GET", "/api/lm/download-progress/{download_id}", "get_download_progress"),
|
||||||
|
RouteDefinition("POST", "/api/lm/{prefix}/cancel-task", "cancel_task"),
|
||||||
RouteDefinition("GET", "/{prefix}", "handle_models_page"),
|
RouteDefinition("GET", "/{prefix}", "handle_models_page"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -36,11 +36,13 @@ class AutoOrganizeResult:
|
|||||||
self.results_truncated: bool = False
|
self.results_truncated: bool = False
|
||||||
self.sample_results: List[Dict[str, Any]] = []
|
self.sample_results: List[Dict[str, Any]] = []
|
||||||
self.is_flat_structure: bool = False
|
self.is_flat_structure: bool = False
|
||||||
|
self.status: str = 'success'
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, Any]:
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
"""Convert result to dictionary"""
|
"""Convert result to dictionary"""
|
||||||
result = {
|
result = {
|
||||||
'success': True,
|
'success': self.status != 'error',
|
||||||
|
'status': self.status,
|
||||||
'message': f'Auto-organize {self.operation_type} completed: {self.success_count} moved, {self.skipped_count} skipped, {self.failure_count} failed out of {self.total} total',
|
'message': f'Auto-organize {self.operation_type} completed: {self.success_count} moved, {self.skipped_count} skipped, {self.failure_count} failed out of {self.total} total',
|
||||||
'summary': {
|
'summary': {
|
||||||
'total': self.total,
|
'total': self.total,
|
||||||
@@ -98,6 +100,8 @@ class ModelFileService:
|
|||||||
result = AutoOrganizeResult()
|
result = AutoOrganizeResult()
|
||||||
source_directories: Set[str] = set()
|
source_directories: Set[str] = set()
|
||||||
|
|
||||||
|
self.scanner.reset_cancellation()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get all models from cache
|
# Get all models from cache
|
||||||
cache = await self.scanner.get_cached_data()
|
cache = await self.scanner.get_cached_data()
|
||||||
@@ -186,6 +190,21 @@ class ModelFileService:
|
|||||||
progress_callback,
|
progress_callback,
|
||||||
source_directories # Pass the set to track source directories
|
source_directories # Pass the set to track source directories
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.scanner.is_cancelled():
|
||||||
|
result.status = 'cancelled'
|
||||||
|
if progress_callback:
|
||||||
|
await progress_callback.on_progress({
|
||||||
|
'type': 'auto_organize_progress',
|
||||||
|
'status': 'cancelled',
|
||||||
|
'total': result.total,
|
||||||
|
'processed': result.processed,
|
||||||
|
'success': result.success_count,
|
||||||
|
'failures': result.failure_count,
|
||||||
|
'skipped': result.skipped_count,
|
||||||
|
'operation_type': result.operation_type
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
# Send cleanup progress
|
# Send cleanup progress
|
||||||
if progress_callback:
|
if progress_callback:
|
||||||
@@ -246,9 +265,15 @@ class ModelFileService:
|
|||||||
"""Process models in batches to avoid overwhelming the system"""
|
"""Process models in batches to avoid overwhelming the system"""
|
||||||
|
|
||||||
for i in range(0, result.total, AUTO_ORGANIZE_BATCH_SIZE):
|
for i in range(0, result.total, AUTO_ORGANIZE_BATCH_SIZE):
|
||||||
|
if self.scanner.is_cancelled():
|
||||||
|
logger.info(f"{self.model_type.capitalize()} File Service: Auto-organize cancelled by user")
|
||||||
|
break
|
||||||
|
|
||||||
batch = all_models[i:i + AUTO_ORGANIZE_BATCH_SIZE]
|
batch = all_models[i:i + AUTO_ORGANIZE_BATCH_SIZE]
|
||||||
|
|
||||||
for model in batch:
|
for model in batch:
|
||||||
|
if self.scanner.is_cancelled():
|
||||||
|
break
|
||||||
await self._process_single_model(model, model_roots, result, source_directories)
|
await self._process_single_model(model, model_roots, result, source_directories)
|
||||||
result.processed += 1
|
result.processed += 1
|
||||||
|
|
||||||
@@ -535,8 +560,12 @@ class ModelMoveService:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
results = []
|
results = []
|
||||||
|
self.scanner.reset_cancellation()
|
||||||
|
|
||||||
for file_path in file_paths:
|
for file_path in file_paths:
|
||||||
|
if self.scanner.is_cancelled():
|
||||||
|
logger.info(f"{self.model_type.capitalize()} Move Service: Bulk move cancelled by user")
|
||||||
|
break
|
||||||
result = await self.move_model(file_path, target_path, use_default_paths=use_default_paths)
|
result = await self.move_model(file_path, target_path, use_default_paths=use_default_paths)
|
||||||
results.append({
|
results.append({
|
||||||
"original_file_path": file_path,
|
"original_file_path": file_path,
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ class ModelScanner:
|
|||||||
self._excluded_models = [] # List to track excluded models
|
self._excluded_models = [] # List to track excluded models
|
||||||
self._persistent_cache = get_persistent_cache()
|
self._persistent_cache = get_persistent_cache()
|
||||||
self._name_display_mode = self._resolve_name_display_mode()
|
self._name_display_mode = self._resolve_name_display_mode()
|
||||||
|
self._cancel_requested = False # Flag for cancellation
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
@@ -678,6 +679,7 @@ class ModelScanner:
|
|||||||
|
|
||||||
async def _reconcile_cache(self) -> None:
|
async def _reconcile_cache(self) -> None:
|
||||||
"""Fast cache reconciliation - only process differences between cache and filesystem"""
|
"""Fast cache reconciliation - only process differences between cache and filesystem"""
|
||||||
|
self.reset_cancellation()
|
||||||
self._is_initializing = True # Set flag for reconciliation duration
|
self._is_initializing = True # Set flag for reconciliation duration
|
||||||
try:
|
try:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
@@ -737,6 +739,9 @@ class ModelScanner:
|
|||||||
|
|
||||||
# Yield control periodically
|
# Yield control periodically
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
|
if self.is_cancelled():
|
||||||
|
logger.info(f"{self.model_type.capitalize()} Scanner: Reconcile scan cancelled")
|
||||||
|
return
|
||||||
|
|
||||||
# Process new files in batches
|
# Process new files in batches
|
||||||
total_added = 0
|
total_added = 0
|
||||||
@@ -784,6 +789,10 @@ class ModelScanner:
|
|||||||
logger.error(f"Could not determine root path for {path}")
|
logger.error(f"Could not determine root path for {path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error adding {path} to cache: {e}")
|
logger.error(f"Error adding {path} to cache: {e}")
|
||||||
|
|
||||||
|
if self.is_cancelled():
|
||||||
|
logger.info(f"{self.model_type.capitalize()} Scanner: Reconcile processing cancelled")
|
||||||
|
return
|
||||||
|
|
||||||
# Find missing files (in cache but not in filesystem)
|
# Find missing files (in cache but not in filesystem)
|
||||||
missing_files = cached_paths - found_paths
|
missing_files = cached_paths - found_paths
|
||||||
@@ -838,6 +847,19 @@ class ModelScanner:
|
|||||||
"""Check if the scanner is currently initializing"""
|
"""Check if the scanner is currently initializing"""
|
||||||
return self._is_initializing
|
return self._is_initializing
|
||||||
|
|
||||||
|
def cancel_task(self) -> None:
|
||||||
|
"""Request cancellation of the current long-running task."""
|
||||||
|
self._cancel_requested = True
|
||||||
|
logger.info(f"{self.model_type.capitalize()} 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
|
||||||
|
|
||||||
def get_model_roots(self) -> List[str]:
|
def get_model_roots(self) -> List[str]:
|
||||||
"""Get model root directories"""
|
"""Get model root directories"""
|
||||||
raise NotImplementedError("Subclasses must implement get_model_roots")
|
raise NotImplementedError("Subclasses must implement get_model_roots")
|
||||||
@@ -1030,6 +1052,8 @@ class ModelScanner:
|
|||||||
except Exception as exc: # pragma: no cover - defensive logging
|
except Exception as exc: # pragma: no cover - defensive logging
|
||||||
logger.error(f"Error reporting progress for {self.model_type}: {exc}")
|
logger.error(f"Error reporting progress for {self.model_type}: {exc}")
|
||||||
|
|
||||||
|
self.reset_cancellation()
|
||||||
|
|
||||||
async def scan_recursive(current_path: str, root_path: str, visited_paths: Set[str]) -> None:
|
async def scan_recursive(current_path: str, root_path: str, visited_paths: Set[str]) -> None:
|
||||||
nonlocal processed_files
|
nonlocal processed_files
|
||||||
|
|
||||||
@@ -1073,6 +1097,8 @@ class ModelScanner:
|
|||||||
|
|
||||||
await handle_progress()
|
await handle_progress()
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
|
if self.is_cancelled():
|
||||||
|
return
|
||||||
elif entry.is_dir(follow_symlinks=True):
|
elif entry.is_dir(follow_symlinks=True):
|
||||||
await scan_recursive(entry.path, root_path, visited_paths)
|
await scan_recursive(entry.path, root_path, visited_paths)
|
||||||
except Exception as entry_error:
|
except Exception as entry_error:
|
||||||
@@ -1080,6 +1106,9 @@ class ModelScanner:
|
|||||||
except Exception as scan_error:
|
except Exception as scan_error:
|
||||||
logger.error(f"Error scanning {current_path}: {scan_error}")
|
logger.error(f"Error scanning {current_path}: {scan_error}")
|
||||||
|
|
||||||
|
if self.is_cancelled():
|
||||||
|
return
|
||||||
|
|
||||||
for model_root in self.get_model_roots():
|
for model_root in self.get_model_roots():
|
||||||
if not os.path.exists(model_root):
|
if not os.path.exists(model_root):
|
||||||
continue
|
continue
|
||||||
@@ -1448,6 +1477,10 @@ class ModelScanner:
|
|||||||
deleted_models = []
|
deleted_models = []
|
||||||
|
|
||||||
for file_path in file_paths:
|
for file_path in file_paths:
|
||||||
|
if self.is_cancelled():
|
||||||
|
logger.info(f"{self.model_type.capitalize()} Scanner: Bulk delete cancelled by user")
|
||||||
|
break
|
||||||
|
|
||||||
try:
|
try:
|
||||||
target_dir = os.path.dirname(file_path)
|
target_dir = os.path.dirname(file_path)
|
||||||
base_name = os.path.basename(file_path)
|
base_name = os.path.basename(file_path)
|
||||||
@@ -1488,6 +1521,7 @@ class ModelScanner:
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
|
'status': 'cancelled' if self.is_cancelled() else 'success',
|
||||||
'total_deleted': total_deleted,
|
'total_deleted': total_deleted,
|
||||||
'total_attempted': len(file_paths),
|
'total_attempted': len(file_paths),
|
||||||
'cache_updated': cache_updated,
|
'cache_updated': cache_updated,
|
||||||
|
|||||||
@@ -466,6 +466,7 @@ class ModelUpdateService:
|
|||||||
target_model_ids: Optional[Sequence[int]] = None,
|
target_model_ids: Optional[Sequence[int]] = None,
|
||||||
) -> Dict[int, ModelUpdateRecord]:
|
) -> Dict[int, ModelUpdateRecord]:
|
||||||
"""Refresh update information for every model present in the cache."""
|
"""Refresh update information for every model present in the cache."""
|
||||||
|
scanner.reset_cancellation()
|
||||||
|
|
||||||
normalized_targets = (
|
normalized_targets = (
|
||||||
self._normalize_sequence(target_model_ids)
|
self._normalize_sequence(target_model_ids)
|
||||||
@@ -542,6 +543,9 @@ class ModelUpdateService:
|
|||||||
force_refresh=force_refresh,
|
force_refresh=force_refresh,
|
||||||
prefetched_response=prefetched.get(model_id),
|
prefetched_response=prefetched.get(model_id),
|
||||||
)
|
)
|
||||||
|
if scanner.is_cancelled():
|
||||||
|
logger.info(f"{model_type.capitalize()} Update Service: Refresh cancelled by user")
|
||||||
|
return results
|
||||||
if record:
|
if record:
|
||||||
results[model_id] = record
|
results[model_id] = record
|
||||||
if index % progress_interval == 0 or index == total_models:
|
if index % progress_interval == 0 or index == total_models:
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ class BulkMetadataRefreshUseCase:
|
|||||||
success = 0
|
success = 0
|
||||||
needs_resort = False
|
needs_resort = False
|
||||||
|
|
||||||
|
self._service.scanner.reset_cancellation()
|
||||||
|
|
||||||
async def emit(status: str, **extra: Any) -> None:
|
async def emit(status: str, **extra: Any) -> None:
|
||||||
if progress_callback is None:
|
if progress_callback is None:
|
||||||
return
|
return
|
||||||
@@ -69,6 +71,10 @@ class BulkMetadataRefreshUseCase:
|
|||||||
await emit("started")
|
await emit("started")
|
||||||
|
|
||||||
for model in to_process:
|
for model in to_process:
|
||||||
|
if self._service.scanner.is_cancelled():
|
||||||
|
self._logger.info("Bulk metadata refresh cancelled by user")
|
||||||
|
await emit("cancelled", processed=processed, success=success)
|
||||||
|
return {"success": False, "message": "Operation cancelled", "processed": processed, "updated": success, "total": total_models}
|
||||||
try:
|
try:
|
||||||
original_name = model.get("model_name")
|
original_name = model.get("model_name")
|
||||||
await MetadataManager.hydrate_model_data(model)
|
await MetadataManager.hydrate_model_data(model)
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export function getApiEndpoints(modelType) {
|
|||||||
if (!Object.values(MODEL_TYPES).includes(modelType)) {
|
if (!Object.values(MODEL_TYPES).includes(modelType)) {
|
||||||
throw new Error(`Invalid model type: ${modelType}`);
|
throw new Error(`Invalid model type: ${modelType}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Base CRUD operations
|
// Base CRUD operations
|
||||||
list: `/api/lm/${modelType}/list`,
|
list: `/api/lm/${modelType}/list`,
|
||||||
@@ -60,17 +60,18 @@ export function getApiEndpoints(modelType) {
|
|||||||
exclude: `/api/lm/${modelType}/exclude`,
|
exclude: `/api/lm/${modelType}/exclude`,
|
||||||
rename: `/api/lm/${modelType}/rename`,
|
rename: `/api/lm/${modelType}/rename`,
|
||||||
save: `/api/lm/${modelType}/save-metadata`,
|
save: `/api/lm/${modelType}/save-metadata`,
|
||||||
|
cancelTask: `/api/lm/${modelType}/cancel-task`,
|
||||||
|
|
||||||
// Bulk operations
|
// Bulk operations
|
||||||
bulkDelete: `/api/lm/${modelType}/bulk-delete`,
|
bulkDelete: `/api/lm/${modelType}/bulk-delete`,
|
||||||
|
|
||||||
// Tag operations
|
// Tag operations
|
||||||
addTags: `/api/lm/${modelType}/add-tags`,
|
addTags: `/api/lm/${modelType}/add-tags`,
|
||||||
|
|
||||||
// Move operations (now common for all model types that support move)
|
// Move operations (now common for all model types that support move)
|
||||||
moveModel: `/api/lm/${modelType}/move_model`,
|
moveModel: `/api/lm/${modelType}/move_model`,
|
||||||
moveBulk: `/api/lm/${modelType}/move_models_bulk`,
|
moveBulk: `/api/lm/${modelType}/move_models_bulk`,
|
||||||
|
|
||||||
// CivitAI integration
|
// CivitAI integration
|
||||||
fetchCivitai: `/api/lm/${modelType}/fetch-civitai`,
|
fetchCivitai: `/api/lm/${modelType}/fetch-civitai`,
|
||||||
fetchAllCivitai: `/api/lm/${modelType}/fetch-all-civitai`,
|
fetchAllCivitai: `/api/lm/${modelType}/fetch-all-civitai`,
|
||||||
@@ -82,10 +83,10 @@ export function getApiEndpoints(modelType) {
|
|||||||
modelUpdateVersions: `/api/lm/${modelType}/updates/versions`,
|
modelUpdateVersions: `/api/lm/${modelType}/updates/versions`,
|
||||||
ignoreModelUpdate: `/api/lm/${modelType}/updates/ignore`,
|
ignoreModelUpdate: `/api/lm/${modelType}/updates/ignore`,
|
||||||
ignoreVersionUpdate: `/api/lm/${modelType}/updates/ignore-version`,
|
ignoreVersionUpdate: `/api/lm/${modelType}/updates/ignore-version`,
|
||||||
|
|
||||||
// Preview management
|
// Preview management
|
||||||
replacePreview: `/api/lm/${modelType}/replace-preview`,
|
replacePreview: `/api/lm/${modelType}/replace-preview`,
|
||||||
|
|
||||||
// Query operations
|
// Query operations
|
||||||
scan: `/api/lm/${modelType}/scan`,
|
scan: `/api/lm/${modelType}/scan`,
|
||||||
topTags: `/api/lm/${modelType}/top-tags`,
|
topTags: `/api/lm/${modelType}/top-tags`,
|
||||||
@@ -99,11 +100,11 @@ export function getApiEndpoints(modelType) {
|
|||||||
verify: `/api/lm/${modelType}/verify-duplicates`,
|
verify: `/api/lm/${modelType}/verify-duplicates`,
|
||||||
metadata: `/api/lm/${modelType}/metadata`,
|
metadata: `/api/lm/${modelType}/metadata`,
|
||||||
modelDescription: `/api/lm/${modelType}/model-description`,
|
modelDescription: `/api/lm/${modelType}/model-description`,
|
||||||
|
|
||||||
// Auto-organize operations
|
// Auto-organize operations
|
||||||
autoOrganize: `/api/lm/${modelType}/auto-organize`,
|
autoOrganize: `/api/lm/${modelType}/auto-organize`,
|
||||||
autoOrganizeProgress: `/api/lm/${modelType}/auto-organize-progress`,
|
autoOrganizeProgress: `/api/lm/${modelType}/auto-organize-progress`,
|
||||||
|
|
||||||
// Model-specific endpoints (will be merged with specific configs)
|
// Model-specific endpoints (will be merged with specific configs)
|
||||||
specific: {}
|
specific: {}
|
||||||
};
|
};
|
||||||
@@ -144,7 +145,7 @@ export function getCompleteApiConfig(modelType) {
|
|||||||
const baseEndpoints = getApiEndpoints(modelType);
|
const baseEndpoints = getApiEndpoints(modelType);
|
||||||
const specificEndpoints = MODEL_SPECIFIC_ENDPOINTS[modelType] || {};
|
const specificEndpoints = MODEL_SPECIFIC_ENDPOINTS[modelType] || {};
|
||||||
const config = MODEL_CONFIG[modelType];
|
const config = MODEL_CONFIG[modelType];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
modelType,
|
modelType,
|
||||||
config,
|
config,
|
||||||
|
|||||||
@@ -82,6 +82,19 @@ export class BaseModelApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancelTask() {
|
||||||
|
try {
|
||||||
|
const endpoint = this.apiConfig.endpoints.cancelTask;
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error cancelling task for ${this.modelType}:`, error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async loadMoreWithVirtualScroll(resetPage = false, updateFolders = false) {
|
async loadMoreWithVirtualScroll(resetPage = false, updateFolders = false) {
|
||||||
const pageState = this.getPageState();
|
const pageState = this.getPageState();
|
||||||
|
|
||||||
@@ -336,9 +349,11 @@ export class BaseModelApiClient {
|
|||||||
|
|
||||||
async refreshModels(fullRebuild = false) {
|
async refreshModels(fullRebuild = false) {
|
||||||
try {
|
try {
|
||||||
state.loadingManager.showSimpleLoading(
|
state.loadingManager.show(
|
||||||
`${fullRebuild ? 'Full rebuild' : 'Refreshing'} ${this.apiConfig.config.displayName}s...`
|
`${fullRebuild ? 'Full rebuild' : 'Refreshing'} ${this.apiConfig.config.displayName}s...`,
|
||||||
|
0
|
||||||
);
|
);
|
||||||
|
state.loadingManager.showCancelButton(() => this.cancelTask());
|
||||||
|
|
||||||
const url = new URL(this.apiConfig.endpoints.scan, window.location.origin);
|
const url = new URL(this.apiConfig.endpoints.scan, window.location.origin);
|
||||||
url.searchParams.append('full_rebuild', fullRebuild);
|
url.searchParams.append('full_rebuild', fullRebuild);
|
||||||
@@ -349,6 +364,12 @@ export class BaseModelApiClient {
|
|||||||
throw new Error(`Failed to refresh ${this.apiConfig.config.displayName}s: ${response.status} ${response.statusText}`);
|
throw new Error(`Failed to refresh ${this.apiConfig.config.displayName}s: ${response.status} ${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.status === 'cancelled') {
|
||||||
|
showToast('toast.api.operationCancelled', {}, 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
resetAndReload(true);
|
resetAndReload(true);
|
||||||
|
|
||||||
showToast('toast.api.refreshComplete', { action: fullRebuild ? 'Full rebuild' : 'Refresh' }, 'success');
|
showToast('toast.api.refreshComplete', { action: fullRebuild ? 'Full rebuild' : 'Refresh' }, 'success');
|
||||||
@@ -402,6 +423,7 @@ export class BaseModelApiClient {
|
|||||||
|
|
||||||
await state.loadingManager.showWithProgress(async (loading) => {
|
await state.loadingManager.showWithProgress(async (loading) => {
|
||||||
try {
|
try {
|
||||||
|
loading.showCancelButton(() => this.cancelTask());
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`);
|
ws = new WebSocket(`${wsProtocol}${window.location.host}${WS_ENDPOINTS.fetchProgress}`);
|
||||||
|
|
||||||
@@ -427,7 +449,12 @@ export class BaseModelApiClient {
|
|||||||
loading.setStatus(
|
loading.setStatus(
|
||||||
`Completed: Updated ${data.success} of ${data.processed} ${this.apiConfig.config.displayName}s`
|
`Completed: Updated ${data.success} of ${data.processed} ${this.apiConfig.config.displayName}s`
|
||||||
);
|
);
|
||||||
resolve();
|
resolve(data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'cancelled':
|
||||||
|
loading.setStatus('Operation cancelled by user');
|
||||||
|
resolve(data); // Consider it complete but marked as cancelled
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
@@ -458,10 +485,14 @@ export class BaseModelApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the operation to complete via WebSocket
|
// Wait for the operation to complete via WebSocket
|
||||||
await operationComplete;
|
const finalData = await operationComplete;
|
||||||
|
|
||||||
resetAndReload(false);
|
resetAndReload(false);
|
||||||
showToast('toast.api.metadataUpdateComplete', {}, 'success');
|
if (finalData && finalData.status === 'cancelled') {
|
||||||
|
showToast('toast.api.operationCancelledPartial', { success: finalData.success, total: finalData.total }, 'info');
|
||||||
|
} else {
|
||||||
|
showToast('toast.api.metadataUpdateComplete', {}, 'success');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching metadata:', error);
|
console.error('Error fetching metadata:', error);
|
||||||
showToast('toast.api.metadataFetchFailed', { message: error.message }, 'error');
|
showToast('toast.api.metadataFetchFailed', { message: error.message }, 'error');
|
||||||
@@ -487,9 +518,17 @@ export class BaseModelApiClient {
|
|||||||
let failedItems = [];
|
let failedItems = [];
|
||||||
|
|
||||||
const progressController = state.loadingManager.showEnhancedProgress('Starting metadata refresh...');
|
const progressController = state.loadingManager.showEnhancedProgress('Starting metadata refresh...');
|
||||||
|
let cancelled = false;
|
||||||
|
progressController.showCancelButton(() => {
|
||||||
|
cancelled = true;
|
||||||
|
this.cancelTask();
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (let i = 0; i < filePaths.length; i++) {
|
for (let i = 0; i < filePaths.length; i++) {
|
||||||
|
if (cancelled) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
const filePath = filePaths[i];
|
const filePath = filePaths[i];
|
||||||
const fileName = filePath.split('/').pop();
|
const fileName = filePath.split('/').pop();
|
||||||
|
|
||||||
@@ -531,20 +570,15 @@ export class BaseModelApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let completionMessage;
|
let completionMessage;
|
||||||
if (successCount === totalItems) {
|
if (cancelled) {
|
||||||
|
completionMessage = translate('toast.api.operationCancelledPartial', { success: successCount, total: totalItems }, `Operation cancelled. ${successCount} items processed.`);
|
||||||
|
showToast('toast.api.operationCancelledPartial', { success: successCount, total: totalItems }, 'info');
|
||||||
|
} else if (successCount === totalItems) {
|
||||||
completionMessage = translate('toast.api.bulkMetadataCompleteAll', { count: successCount, type: this.apiConfig.config.displayName }, `Successfully refreshed all ${successCount} ${this.apiConfig.config.displayName}s`);
|
completionMessage = translate('toast.api.bulkMetadataCompleteAll', { count: successCount, type: this.apiConfig.config.displayName }, `Successfully refreshed all ${successCount} ${this.apiConfig.config.displayName}s`);
|
||||||
showToast('toast.api.bulkMetadataCompleteAll', { count: successCount, type: this.apiConfig.config.displayName }, 'success');
|
showToast('toast.api.bulkMetadataCompleteAll', { count: successCount, type: this.apiConfig.config.displayName }, 'success');
|
||||||
} else if (successCount > 0) {
|
} else if (successCount > 0) {
|
||||||
completionMessage = translate('toast.api.bulkMetadataCompletePartial', { success: successCount, total: totalItems, type: this.apiConfig.config.displayName }, `Refreshed ${successCount} of ${totalItems} ${this.apiConfig.config.displayName}s`);
|
completionMessage = translate('toast.api.bulkMetadataCompletePartial', { success: successCount, total: totalItems, type: this.apiConfig.config.displayName }, `Refreshed ${successCount} of ${totalItems} ${this.apiConfig.config.displayName}s`);
|
||||||
showToast('toast.api.bulkMetadataCompletePartial', { success: successCount, total: totalItems, type: this.apiConfig.config.displayName }, 'warning');
|
showToast('toast.api.bulkMetadataCompletePartial', { success: successCount, total: totalItems, type: this.apiConfig.config.displayName }, 'warning');
|
||||||
|
|
||||||
// if (failedItems.length > 0) {
|
|
||||||
// const failureMessage = failedItems.length <= 3
|
|
||||||
// ? failedItems.map(item => `${item.fileName}: ${item.error}`).join('\n')
|
|
||||||
// : failedItems.slice(0, 3).map(item => `${item.fileName}: ${item.error}`).join('\n') +
|
|
||||||
// `\n(and ${failedItems.length - 3} more)`;
|
|
||||||
// showToast('toast.api.bulkMetadataFailureDetails', { failures: failureMessage }, 'warning', 6000);
|
|
||||||
// }
|
|
||||||
} else {
|
} else {
|
||||||
completionMessage = translate('toast.api.bulkMetadataCompleteNone', { type: this.apiConfig.config.displayName }, `Failed to refresh metadata for any ${this.apiConfig.config.displayName}s`);
|
completionMessage = translate('toast.api.bulkMetadataCompleteNone', { type: this.apiConfig.config.displayName }, `Failed to refresh metadata for any ${this.apiConfig.config.displayName}s`);
|
||||||
showToast('toast.api.bulkMetadataCompleteNone', { type: this.apiConfig.config.displayName }, 'error');
|
showToast('toast.api.bulkMetadataCompleteNone', { type: this.apiConfig.config.displayName }, 'error');
|
||||||
@@ -574,28 +608,42 @@ export class BaseModelApiClient {
|
|||||||
throw new Error('No model IDs provided');
|
throw new Error('No model IDs provided');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(this.apiConfig.endpoints.refreshUpdates, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
model_ids: modelIds,
|
|
||||||
force
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
let payload = {};
|
|
||||||
try {
|
try {
|
||||||
payload = await response.json();
|
state.loadingManager.show('Checking for updates...', 0);
|
||||||
|
state.loadingManager.showCancelButton(() => this.cancelTask());
|
||||||
|
|
||||||
|
const response = await fetch(this.apiConfig.endpoints.refreshUpdates, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model_ids: modelIds,
|
||||||
|
force
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let payload = {};
|
||||||
|
try {
|
||||||
|
payload = await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Unable to parse refresh updates response as JSON', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok || payload?.success !== true) {
|
||||||
|
if (payload?.status === 'cancelled') {
|
||||||
|
showToast('toast.api.operationCancelled', {}, 'info');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const message = payload?.error || response.statusText || 'Failed to refresh updates';
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Unable to parse refresh updates response as JSON', error);
|
console.error('Error refreshing updates for models:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
state.loadingManager.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok || payload?.success !== true) {
|
|
||||||
const message = payload?.error || response.statusText || 'Failed to refresh updates';
|
|
||||||
throw new Error(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchCivitaiVersions(modelId, source = null) {
|
async fetchCivitaiVersions(modelId, source = null) {
|
||||||
@@ -1016,6 +1064,7 @@ export class BaseModelApiClient {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
state.loadingManager.showSimpleLoading(`Deleting ${this.apiConfig.config.displayName.toLowerCase()}s...`);
|
state.loadingManager.showSimpleLoading(`Deleting ${this.apiConfig.config.displayName.toLowerCase()}s...`);
|
||||||
|
state.loadingManager.showCancelButton(() => this.cancelTask());
|
||||||
|
|
||||||
const response = await fetch(this.apiConfig.endpoints.bulkDelete, {
|
const response = await fetch(this.apiConfig.endpoints.bulkDelete, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -1055,6 +1104,7 @@ export class BaseModelApiClient {
|
|||||||
let ws = null;
|
let ws = null;
|
||||||
|
|
||||||
await state.loadingManager.showWithProgress(async (loading) => {
|
await state.loadingManager.showWithProgress(async (loading) => {
|
||||||
|
loading.showCancelButton(() => this.stopExampleImages());
|
||||||
try {
|
try {
|
||||||
// Connect to WebSocket for progress updates
|
// Connect to WebSocket for progress updates
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
@@ -1202,6 +1252,7 @@ export class BaseModelApiClient {
|
|||||||
let ws = null;
|
let ws = null;
|
||||||
|
|
||||||
await state.loadingManager.showWithProgress(async (loading) => {
|
await state.loadingManager.showWithProgress(async (loading) => {
|
||||||
|
loading.showCancelButton(() => this.cancelTask());
|
||||||
try {
|
try {
|
||||||
// Connect to WebSocket for progress updates
|
// Connect to WebSocket for progress updates
|
||||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
@@ -1255,6 +1306,11 @@ export class BaseModelApiClient {
|
|||||||
}, 1500);
|
}, 1500);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'cancelled':
|
||||||
|
loading.setStatus(translate('toast.api.operationCancelled', {}, 'Operation cancelled by user'));
|
||||||
|
resolve(data);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'error':
|
case 'error':
|
||||||
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.error', { error: data.error }, `Error: ${data.error}`));
|
loading.setStatus(translate('loras.bulkOperations.autoOrganizeProgress.error', { error: data.error }, `Error: ${data.error}`));
|
||||||
reject(new Error(data.error));
|
reject(new Error(data.error));
|
||||||
@@ -1299,7 +1355,9 @@ export class BaseModelApiClient {
|
|||||||
const result = await operationComplete;
|
const result = await operationComplete;
|
||||||
|
|
||||||
// Show appropriate success message based on results
|
// Show appropriate success message based on results
|
||||||
if (result.failures === 0) {
|
if (result.status === 'cancelled') {
|
||||||
|
showToast('toast.api.operationCancelledPartial', { success: result.success, total: result.total }, 'info');
|
||||||
|
} else if (result.failures === 0) {
|
||||||
showToast('toast.loras.autoOrganizeSuccess', {
|
showToast('toast.loras.autoOrganizeSuccess', {
|
||||||
count: result.success,
|
count: result.success,
|
||||||
type: result.operation_type === 'bulk' ? 'selected models' : 'all models'
|
type: result.operation_type === 'bulk' ? 'selected models' : 'all models'
|
||||||
@@ -1326,4 +1384,17 @@ export class BaseModelApiClient {
|
|||||||
completionMessage: translate('loras.bulkOperations.autoOrganizeProgress.complete', {}, 'Auto-organize complete')
|
completionMessage: translate('loras.bulkOperations.autoOrganizeProgress.complete', {}, 'Auto-organize complete')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async stopExampleImages() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/lm/stop-example-images', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
return response.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error stopping example images:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,19 +16,19 @@ export class BulkManager {
|
|||||||
this.bulkBtn = document.getElementById('bulkOperationsBtn');
|
this.bulkBtn = document.getElementById('bulkOperationsBtn');
|
||||||
// Remove bulk panel references since we're using context menu now
|
// Remove bulk panel references since we're using context menu now
|
||||||
this.bulkContextMenu = null; // Will be set by core initialization
|
this.bulkContextMenu = null; // Will be set by core initialization
|
||||||
|
|
||||||
// Marquee selection properties
|
// Marquee selection properties
|
||||||
this.isMarqueeActive = false;
|
this.isMarqueeActive = false;
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
this.marqueeStart = { x: 0, y: 0 };
|
this.marqueeStart = { x: 0, y: 0 };
|
||||||
this.marqueeElement = null;
|
this.marqueeElement = null;
|
||||||
this.initialSelectedModels = new Set();
|
this.initialSelectedModels = new Set();
|
||||||
|
|
||||||
// Drag detection properties
|
// Drag detection properties
|
||||||
this.dragThreshold = 5; // Pixels to move before considering it a drag
|
this.dragThreshold = 5; // Pixels to move before considering it a drag
|
||||||
this.mouseDownTime = 0;
|
this.mouseDownTime = 0;
|
||||||
this.mouseDownPosition = { x: 0, y: 0 };
|
this.mouseDownPosition = { x: 0, y: 0 };
|
||||||
|
|
||||||
// Model type specific action configurations
|
// Model type specific action configurations
|
||||||
this.actionConfig = {
|
this.actionConfig = {
|
||||||
[MODEL_TYPES.LORA]: {
|
[MODEL_TYPES.LORA]: {
|
||||||
@@ -103,7 +103,7 @@ export class BulkManager {
|
|||||||
initialize() {
|
initialize() {
|
||||||
// Register with event manager for coordinated event handling
|
// Register with event manager for coordinated event handling
|
||||||
this.registerEventHandlers();
|
this.registerEventHandlers();
|
||||||
|
|
||||||
// Initialize bulk mode state in event manager
|
// Initialize bulk mode state in event manager
|
||||||
eventManager.setState('bulkMode', state.bulkMode || false);
|
eventManager.setState('bulkMode', state.bulkMode || false);
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ export class BulkManager {
|
|||||||
const dx = e.clientX - this.mouseDownPosition.x;
|
const dx = e.clientX - this.mouseDownPosition.x;
|
||||||
const dy = e.clientY - this.mouseDownPosition.y;
|
const dy = e.clientY - this.mouseDownPosition.y;
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
if (distance >= this.dragThreshold) {
|
if (distance >= this.dragThreshold) {
|
||||||
this.isDragging = true;
|
this.isDragging = true;
|
||||||
this.startMarqueeSelection(e, true);
|
this.startMarqueeSelection(e, true);
|
||||||
@@ -176,7 +176,7 @@ export class BulkManager {
|
|||||||
this.endMarqueeSelection(e);
|
this.endMarqueeSelection(e);
|
||||||
return true; // Stop propagation
|
return true; // Stop propagation
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset drag detection if we had a mousedown but didn't drag
|
// Reset drag detection if we had a mousedown but didn't drag
|
||||||
if (this.mouseDownTime) {
|
if (this.mouseDownTime) {
|
||||||
this.mouseDownTime = 0;
|
this.mouseDownTime = 0;
|
||||||
@@ -258,25 +258,25 @@ export class BulkManager {
|
|||||||
this.toggleBulkMode();
|
this.toggleBulkMode();
|
||||||
return true; // Stop propagation
|
return true; // Stop propagation
|
||||||
}
|
}
|
||||||
|
|
||||||
return false; // Continue with other handlers
|
return false; // Continue with other handlers
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleBulkMode() {
|
toggleBulkMode() {
|
||||||
state.bulkMode = !state.bulkMode;
|
state.bulkMode = !state.bulkMode;
|
||||||
|
|
||||||
// Update event manager state
|
// Update event manager state
|
||||||
eventManager.setState('bulkMode', state.bulkMode);
|
eventManager.setState('bulkMode', state.bulkMode);
|
||||||
|
|
||||||
if (this.bulkBtn) {
|
if (this.bulkBtn) {
|
||||||
this.bulkBtn.classList.toggle('active', state.bulkMode);
|
this.bulkBtn.classList.toggle('active', state.bulkMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCardsForBulkMode(state.bulkMode);
|
updateCardsForBulkMode(state.bulkMode);
|
||||||
|
|
||||||
if (!state.bulkMode) {
|
if (!state.bulkMode) {
|
||||||
this.clearSelection();
|
this.clearSelection();
|
||||||
|
|
||||||
// Hide context menu when exiting bulk mode
|
// Hide context menu when exiting bulk mode
|
||||||
if (this.bulkContextMenu) {
|
if (this.bulkContextMenu) {
|
||||||
this.bulkContextMenu.hideMenu();
|
this.bulkContextMenu.hideMenu();
|
||||||
@@ -289,7 +289,7 @@ export class BulkManager {
|
|||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
});
|
});
|
||||||
state.selectedModels.clear();
|
state.selectedModels.clear();
|
||||||
|
|
||||||
// Update context menu header if visible
|
// Update context menu header if visible
|
||||||
if (this.bulkContextMenu) {
|
if (this.bulkContextMenu) {
|
||||||
this.bulkContextMenu.updateSelectedCountHeader();
|
this.bulkContextMenu.updateSelectedCountHeader();
|
||||||
@@ -298,7 +298,7 @@ export class BulkManager {
|
|||||||
|
|
||||||
toggleCardSelection(card) {
|
toggleCardSelection(card) {
|
||||||
const filepath = card.dataset.filepath;
|
const filepath = card.dataset.filepath;
|
||||||
|
|
||||||
if (card.classList.contains('selected')) {
|
if (card.classList.contains('selected')) {
|
||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
state.selectedModels.delete(filepath);
|
state.selectedModels.delete(filepath);
|
||||||
@@ -309,7 +309,7 @@ export class BulkManager {
|
|||||||
// Cache the metadata for this model
|
// Cache the metadata for this model
|
||||||
this.updateMetadataCacheFromCard(filepath, card);
|
this.updateMetadataCacheFromCard(filepath, card);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update context menu header if visible
|
// Update context menu header if visible
|
||||||
if (this.bulkContextMenu) {
|
if (this.bulkContextMenu) {
|
||||||
this.bulkContextMenu.updateSelectedCountHeader();
|
this.bulkContextMenu.updateSelectedCountHeader();
|
||||||
@@ -419,7 +419,7 @@ export class BulkManager {
|
|||||||
|
|
||||||
applySelectionState() {
|
applySelectionState() {
|
||||||
if (!state.bulkMode) return;
|
if (!state.bulkMode) return;
|
||||||
|
|
||||||
document.querySelectorAll('.model-card').forEach(card => {
|
document.querySelectorAll('.model-card').forEach(card => {
|
||||||
const filepath = card.dataset.filepath;
|
const filepath = card.dataset.filepath;
|
||||||
if (state.selectedModels.has(filepath)) {
|
if (state.selectedModels.has(filepath)) {
|
||||||
@@ -437,19 +437,19 @@ export class BulkManager {
|
|||||||
showToast('toast.loras.copyOnlyForLoras', {}, 'warning');
|
showToast('toast.loras.copyOnlyForLoras', {}, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.selectedModels.size === 0) {
|
if (state.selectedModels.size === 0) {
|
||||||
showToast('toast.loras.noLorasSelected', {}, 'warning');
|
showToast('toast.loras.noLorasSelected', {}, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loraSyntaxes = [];
|
const loraSyntaxes = [];
|
||||||
const missingLoras = [];
|
const missingLoras = [];
|
||||||
const metadataCache = this.getMetadataCache();
|
const metadataCache = this.getMetadataCache();
|
||||||
|
|
||||||
for (const filepath of state.selectedModels) {
|
for (const filepath of state.selectedModels) {
|
||||||
const metadata = metadataCache.get(filepath);
|
const metadata = metadataCache.get(filepath);
|
||||||
|
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
const usageTips = JSON.parse(metadata.usageTips || '{}');
|
const usageTips = JSON.parse(metadata.usageTips || '{}');
|
||||||
loraSyntaxes.push(buildLoraSyntax(metadata.fileName, usageTips));
|
loraSyntaxes.push(buildLoraSyntax(metadata.fileName, usageTips));
|
||||||
@@ -457,38 +457,38 @@ export class BulkManager {
|
|||||||
missingLoras.push(filepath);
|
missingLoras.push(filepath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (missingLoras.length > 0) {
|
if (missingLoras.length > 0) {
|
||||||
console.warn('Missing metadata for some selected loras:', missingLoras);
|
console.warn('Missing metadata for some selected loras:', missingLoras);
|
||||||
showToast('toast.loras.missingDataForLoras', { count: missingLoras.length }, 'warning');
|
showToast('toast.loras.missingDataForLoras', { count: missingLoras.length }, 'warning');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loraSyntaxes.length === 0) {
|
if (loraSyntaxes.length === 0) {
|
||||||
showToast('toast.loras.noValidLorasToCopy', {}, 'error');
|
showToast('toast.loras.noValidLorasToCopy', {}, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await copyToClipboard(loraSyntaxes.join(', '), `Copied ${loraSyntaxes.length} LoRA syntaxes to clipboard`);
|
await copyToClipboard(loraSyntaxes.join(', '), `Copied ${loraSyntaxes.length} LoRA syntaxes to clipboard`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendAllModelsToWorkflow(replaceMode = false) {
|
async sendAllModelsToWorkflow(replaceMode = false) {
|
||||||
if (state.currentPageType !== MODEL_TYPES.LORA) {
|
if (state.currentPageType !== MODEL_TYPES.LORA) {
|
||||||
showToast('toast.loras.sendOnlyForLoras', {}, 'warning');
|
showToast('toast.loras.sendOnlyForLoras', {}, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.selectedModels.size === 0) {
|
if (state.selectedModels.size === 0) {
|
||||||
showToast('toast.loras.noLorasSelected', {}, 'warning');
|
showToast('toast.loras.noLorasSelected', {}, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loraSyntaxes = [];
|
const loraSyntaxes = [];
|
||||||
const missingLoras = [];
|
const missingLoras = [];
|
||||||
const metadataCache = this.getMetadataCache();
|
const metadataCache = this.getMetadataCache();
|
||||||
|
|
||||||
for (const filepath of state.selectedModels) {
|
for (const filepath of state.selectedModels) {
|
||||||
const metadata = metadataCache.get(filepath);
|
const metadata = metadataCache.get(filepath);
|
||||||
|
|
||||||
if (metadata) {
|
if (metadata) {
|
||||||
const usageTips = JSON.parse(metadata.usageTips || '{}');
|
const usageTips = JSON.parse(metadata.usageTips || '{}');
|
||||||
loraSyntaxes.push(buildLoraSyntax(metadata.fileName, usageTips));
|
loraSyntaxes.push(buildLoraSyntax(metadata.fileName, usageTips));
|
||||||
@@ -496,56 +496,56 @@ export class BulkManager {
|
|||||||
missingLoras.push(filepath);
|
missingLoras.push(filepath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (missingLoras.length > 0) {
|
if (missingLoras.length > 0) {
|
||||||
console.warn('Missing metadata for some selected loras:', missingLoras);
|
console.warn('Missing metadata for some selected loras:', missingLoras);
|
||||||
showToast('toast.loras.missingDataForLoras', { count: missingLoras.length }, 'warning');
|
showToast('toast.loras.missingDataForLoras', { count: missingLoras.length }, 'warning');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loraSyntaxes.length === 0) {
|
if (loraSyntaxes.length === 0) {
|
||||||
showToast('toast.loras.noValidLorasToSend', {}, 'error');
|
showToast('toast.loras.noValidLorasToSend', {}, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sendLoraToWorkflow(loraSyntaxes.join(', '), replaceMode, 'lora');
|
await sendLoraToWorkflow(loraSyntaxes.join(', '), replaceMode, 'lora');
|
||||||
}
|
}
|
||||||
|
|
||||||
showBulkDeleteModal() {
|
showBulkDeleteModal() {
|
||||||
if (state.selectedModels.size === 0) {
|
if (state.selectedModels.size === 0) {
|
||||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const countElement = document.getElementById('bulkDeleteCount');
|
const countElement = document.getElementById('bulkDeleteCount');
|
||||||
if (countElement) {
|
if (countElement) {
|
||||||
countElement.textContent = state.selectedModels.size;
|
countElement.textContent = state.selectedModels.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
modalManager.showModal('bulkDeleteModal');
|
modalManager.showModal('bulkDeleteModal');
|
||||||
}
|
}
|
||||||
|
|
||||||
async confirmBulkDelete() {
|
async confirmBulkDelete() {
|
||||||
if (state.selectedModels.size === 0) {
|
if (state.selectedModels.size === 0) {
|
||||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
modalManager.closeModal('bulkDeleteModal');
|
modalManager.closeModal('bulkDeleteModal');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
modalManager.closeModal('bulkDeleteModal');
|
modalManager.closeModal('bulkDeleteModal');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiClient = this.getActiveApiClient();
|
const apiClient = this.getActiveApiClient();
|
||||||
const filePaths = Array.from(state.selectedModels);
|
const filePaths = Array.from(state.selectedModels);
|
||||||
|
|
||||||
const result = await apiClient.bulkDeleteModels(filePaths);
|
const result = await apiClient.bulkDeleteModels(filePaths);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const currentConfig = this.getCurrentDisplayConfig();
|
const currentConfig = this.getCurrentDisplayConfig();
|
||||||
showToast('toast.models.deletedSuccessfully', {
|
showToast('toast.models.deletedSuccessfully', {
|
||||||
count: result.deleted_count,
|
count: result.deleted_count,
|
||||||
type: currentConfig.displayName.toLowerCase()
|
type: currentConfig.displayName.toLowerCase()
|
||||||
}, 'success');
|
}, 'success');
|
||||||
|
|
||||||
filePaths.forEach(path => {
|
filePaths.forEach(path => {
|
||||||
state.virtualScroller.removeItemByFilePath(path);
|
state.virtualScroller.removeItemByFilePath(path);
|
||||||
});
|
});
|
||||||
@@ -562,13 +562,13 @@ export class BulkManager {
|
|||||||
showToast('toast.models.deleteFailedGeneral', {}, 'error');
|
showToast('toast.models.deleteFailedGeneral', {}, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deselectItem(filepath) {
|
deselectItem(filepath) {
|
||||||
const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`);
|
const card = document.querySelector(`.model-card[data-filepath="${filepath}"]`);
|
||||||
if (card) {
|
if (card) {
|
||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
state.selectedModels.delete(filepath);
|
state.selectedModels.delete(filepath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,10 +577,10 @@ export class BulkManager {
|
|||||||
showToast('toast.bulk.unableToSelectAll', {}, 'error');
|
showToast('toast.bulk.unableToSelectAll', {}, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldCount = state.selectedModels.size;
|
const oldCount = state.selectedModels.size;
|
||||||
const metadataCache = this.getMetadataCache();
|
const metadataCache = this.getMetadataCache();
|
||||||
|
|
||||||
state.virtualScroller.items.forEach(item => {
|
state.virtualScroller.items.forEach(item => {
|
||||||
if (item && item.file_path) {
|
if (item && item.file_path) {
|
||||||
state.selectedModels.add(item.file_path);
|
state.selectedModels.add(item.file_path);
|
||||||
@@ -596,16 +596,16 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.applySelectionState();
|
this.applySelectionState();
|
||||||
|
|
||||||
const newlySelected = state.selectedModels.size - oldCount;
|
const newlySelected = state.selectedModels.size - oldCount;
|
||||||
const currentConfig = this.getCurrentDisplayConfig();
|
const currentConfig = this.getCurrentDisplayConfig();
|
||||||
showToast('toast.models.selectedAdditional', {
|
showToast('toast.models.selectedAdditional', {
|
||||||
count: newlySelected,
|
count: newlySelected,
|
||||||
type: currentConfig.displayName.toLowerCase()
|
type: currentConfig.displayName.toLowerCase()
|
||||||
}, 'success');
|
}, 'success');
|
||||||
|
|
||||||
if (this.isStripVisible) {
|
if (this.isStripVisible) {
|
||||||
this.updateThumbnailStrip();
|
this.updateThumbnailStrip();
|
||||||
}
|
}
|
||||||
@@ -616,13 +616,13 @@ export class BulkManager {
|
|||||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiClient = getModelApiClient();
|
const apiClient = getModelApiClient();
|
||||||
const filePaths = Array.from(state.selectedModels);
|
const filePaths = Array.from(state.selectedModels);
|
||||||
|
|
||||||
const result = await apiClient.refreshBulkModelMetadata(filePaths);
|
const result = await apiClient.refreshBulkModelMetadata(filePaths);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
const metadataCache = this.getMetadataCache();
|
const metadataCache = this.getMetadataCache();
|
||||||
for (const filepath of state.selectedModels) {
|
for (const filepath of state.selectedModels) {
|
||||||
@@ -634,12 +634,12 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isStripVisible) {
|
if (this.isStripVisible) {
|
||||||
this.updateThumbnailStrip();
|
this.updateThumbnailStrip();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during bulk metadata refresh:', error);
|
console.error('Error during bulk metadata refresh:', error);
|
||||||
showToast('toast.models.refreshMetadataFailed', {}, 'error');
|
showToast('toast.models.refreshMetadataFailed', {}, 'error');
|
||||||
@@ -714,27 +714,27 @@ export class BulkManager {
|
|||||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const countElement = document.getElementById('bulkAddTagsCount');
|
const countElement = document.getElementById('bulkAddTagsCount');
|
||||||
if (countElement) {
|
if (countElement) {
|
||||||
countElement.textContent = state.selectedModels.size;
|
countElement.textContent = state.selectedModels.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear any existing tags in the modal
|
// Clear any existing tags in the modal
|
||||||
const tagsContainer = document.getElementById('bulkTagsItems');
|
const tagsContainer = document.getElementById('bulkTagsItems');
|
||||||
if (tagsContainer) {
|
if (tagsContainer) {
|
||||||
tagsContainer.innerHTML = '';
|
tagsContainer.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
modalManager.showModal('bulkAddTagsModal', null, null, () => {
|
modalManager.showModal('bulkAddTagsModal', null, null, () => {
|
||||||
// Cleanup when modal is closed
|
// Cleanup when modal is closed
|
||||||
this.cleanupBulkAddTagsModal();
|
this.cleanupBulkAddTagsModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize the bulk tags editing interface
|
// Initialize the bulk tags editing interface
|
||||||
this.initializeBulkTagsInterface();
|
this.initializeBulkTagsInterface();
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeBulkTagsInterface() {
|
initializeBulkTagsInterface() {
|
||||||
// Setup tag input behavior
|
// Setup tag input behavior
|
||||||
const tagInput = document.querySelector('.bulk-metadata-input');
|
const tagInput = document.querySelector('.bulk-metadata-input');
|
||||||
@@ -749,31 +749,31 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create suggestions dropdown
|
// Create suggestions dropdown
|
||||||
const tagForm = document.querySelector('#bulkAddTagsModal .metadata-add-form');
|
const tagForm = document.querySelector('#bulkAddTagsModal .metadata-add-form');
|
||||||
if (tagForm) {
|
if (tagForm) {
|
||||||
const suggestionsDropdown = this.createBulkSuggestionsDropdown();
|
const suggestionsDropdown = this.createBulkSuggestionsDropdown();
|
||||||
tagForm.appendChild(suggestionsDropdown);
|
tagForm.appendChild(suggestionsDropdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup save button
|
// Setup save button
|
||||||
const appendBtn = document.querySelector('.bulk-append-tags-btn');
|
const appendBtn = document.querySelector('.bulk-append-tags-btn');
|
||||||
const replaceBtn = document.querySelector('.bulk-replace-tags-btn');
|
const replaceBtn = document.querySelector('.bulk-replace-tags-btn');
|
||||||
|
|
||||||
if (appendBtn) {
|
if (appendBtn) {
|
||||||
appendBtn.addEventListener('click', () => {
|
appendBtn.addEventListener('click', () => {
|
||||||
this.saveBulkTags('append');
|
this.saveBulkTags('append');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (replaceBtn) {
|
if (replaceBtn) {
|
||||||
replaceBtn.addEventListener('click', () => {
|
replaceBtn.addEventListener('click', () => {
|
||||||
this.saveBulkTags('replace');
|
this.saveBulkTags('replace');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createBulkSuggestionsDropdown() {
|
createBulkSuggestionsDropdown() {
|
||||||
const dropdown = document.createElement('div');
|
const dropdown = document.createElement('div');
|
||||||
dropdown.className = 'metadata-suggestions-dropdown';
|
dropdown.className = 'metadata-suggestions-dropdown';
|
||||||
@@ -841,34 +841,34 @@ export class BulkManager {
|
|||||||
container.appendChild(item);
|
container.appendChild(item);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addBulkTag(tag) {
|
addBulkTag(tag) {
|
||||||
tag = tag.trim().toLowerCase();
|
tag = tag.trim().toLowerCase();
|
||||||
if (!tag) return;
|
if (!tag) return;
|
||||||
|
|
||||||
const tagsContainer = document.getElementById('bulkTagsItems');
|
const tagsContainer = document.getElementById('bulkTagsItems');
|
||||||
if (!tagsContainer) return;
|
if (!tagsContainer) return;
|
||||||
|
|
||||||
// Validation: Check length
|
// Validation: Check length
|
||||||
if (tag.length > 30) {
|
if (tag.length > 30) {
|
||||||
showToast('modelTags.validation.maxLength', {}, 'error');
|
showToast('modelTags.validation.maxLength', {}, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation: Check total number
|
// Validation: Check total number
|
||||||
const currentTags = tagsContainer.querySelectorAll('.metadata-item');
|
const currentTags = tagsContainer.querySelectorAll('.metadata-item');
|
||||||
if (currentTags.length >= 30) {
|
if (currentTags.length >= 30) {
|
||||||
showToast('modelTags.validation.maxCount', {}, 'error');
|
showToast('modelTags.validation.maxCount', {}, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation: Check for duplicates
|
// Validation: Check for duplicates
|
||||||
const existingTags = Array.from(currentTags).map(tagEl => tagEl.dataset.tag);
|
const existingTags = Array.from(currentTags).map(tagEl => tagEl.dataset.tag);
|
||||||
if (existingTags.includes(tag)) {
|
if (existingTags.includes(tag)) {
|
||||||
showToast('modelTags.validation.duplicate', {}, 'error');
|
showToast('modelTags.validation.duplicate', {}, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new tag
|
// Create new tag
|
||||||
const newTag = document.createElement('div');
|
const newTag = document.createElement('div');
|
||||||
newTag.className = 'metadata-item';
|
newTag.className = 'metadata-item';
|
||||||
@@ -879,7 +879,7 @@ export class BulkManager {
|
|||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Add delete button event listener
|
// Add delete button event listener
|
||||||
const deleteBtn = newTag.querySelector('.metadata-delete-btn');
|
const deleteBtn = newTag.querySelector('.metadata-delete-btn');
|
||||||
deleteBtn.addEventListener('click', (e) => {
|
deleteBtn.addEventListener('click', (e) => {
|
||||||
@@ -888,10 +888,10 @@ export class BulkManager {
|
|||||||
// Update dropdown to show/hide added indicator
|
// Update dropdown to show/hide added indicator
|
||||||
this.updateBulkSuggestionsDropdown();
|
this.updateBulkSuggestionsDropdown();
|
||||||
});
|
});
|
||||||
|
|
||||||
tagsContainer.appendChild(newTag);
|
tagsContainer.appendChild(newTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get existing tags in the bulk tags container
|
* Get existing tags in the bulk tags container
|
||||||
* @returns {Array} Array of existing tag strings
|
* @returns {Array} Array of existing tag strings
|
||||||
@@ -899,29 +899,29 @@ export class BulkManager {
|
|||||||
getBulkExistingTags() {
|
getBulkExistingTags() {
|
||||||
const tagsContainer = document.getElementById('bulkTagsItems');
|
const tagsContainer = document.getElementById('bulkTagsItems');
|
||||||
if (!tagsContainer) return [];
|
if (!tagsContainer) return [];
|
||||||
|
|
||||||
const currentTags = tagsContainer.querySelectorAll('.metadata-item');
|
const currentTags = tagsContainer.querySelectorAll('.metadata-item');
|
||||||
return Array.from(currentTags).map(tag => tag.dataset.tag);
|
return Array.from(currentTags).map(tag => tag.dataset.tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update status of items in the bulk suggestions dropdown
|
* Update status of items in the bulk suggestions dropdown
|
||||||
*/
|
*/
|
||||||
updateBulkSuggestionsDropdown() {
|
updateBulkSuggestionsDropdown() {
|
||||||
const dropdown = document.querySelector('.metadata-suggestions-dropdown');
|
const dropdown = document.querySelector('.metadata-suggestions-dropdown');
|
||||||
if (!dropdown) return;
|
if (!dropdown) return;
|
||||||
|
|
||||||
// Get all current tags
|
// Get all current tags
|
||||||
const existingTags = this.getBulkExistingTags();
|
const existingTags = this.getBulkExistingTags();
|
||||||
|
|
||||||
// Update status of each item in dropdown
|
// Update status of each item in dropdown
|
||||||
dropdown.querySelectorAll('.metadata-suggestion-item').forEach(item => {
|
dropdown.querySelectorAll('.metadata-suggestion-item').forEach(item => {
|
||||||
const tagText = item.querySelector('.metadata-suggestion-text').textContent;
|
const tagText = item.querySelector('.metadata-suggestion-text').textContent;
|
||||||
const isAdded = existingTags.includes(tagText);
|
const isAdded = existingTags.includes(tagText);
|
||||||
|
|
||||||
if (isAdded) {
|
if (isAdded) {
|
||||||
item.classList.add('already-added');
|
item.classList.add('already-added');
|
||||||
|
|
||||||
// Add indicator if it doesn't exist
|
// Add indicator if it doesn't exist
|
||||||
let indicator = item.querySelector('.added-indicator');
|
let indicator = item.querySelector('.added-indicator');
|
||||||
if (!indicator) {
|
if (!indicator) {
|
||||||
@@ -930,18 +930,18 @@ export class BulkManager {
|
|||||||
indicator.innerHTML = '<i class="fas fa-check"></i>';
|
indicator.innerHTML = '<i class="fas fa-check"></i>';
|
||||||
item.appendChild(indicator);
|
item.appendChild(indicator);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove click event
|
// Remove click event
|
||||||
item.onclick = null;
|
item.onclick = null;
|
||||||
item.removeEventListener('click', item._clickHandler);
|
item.removeEventListener('click', item._clickHandler);
|
||||||
} else {
|
} else {
|
||||||
// Re-enable items that are no longer in the list
|
// Re-enable items that are no longer in the list
|
||||||
item.classList.remove('already-added');
|
item.classList.remove('already-added');
|
||||||
|
|
||||||
// Remove indicator if it exists
|
// Remove indicator if it exists
|
||||||
const indicator = item.querySelector('.added-indicator');
|
const indicator = item.querySelector('.added-indicator');
|
||||||
if (indicator) indicator.remove();
|
if (indicator) indicator.remove();
|
||||||
|
|
||||||
// Restore click event if not already set
|
// Restore click event if not already set
|
||||||
if (!item._clickHandler) {
|
if (!item._clickHandler) {
|
||||||
item._clickHandler = () => {
|
item._clickHandler = () => {
|
||||||
@@ -959,29 +959,39 @@ export class BulkManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveBulkTags(mode = 'append') {
|
async saveBulkTags(mode = 'append') {
|
||||||
const tagElements = document.querySelectorAll('#bulkTagsItems .metadata-item');
|
const tagElements = document.querySelectorAll('#bulkTagsItems .metadata-item');
|
||||||
const tags = Array.from(tagElements).map(tag => tag.dataset.tag);
|
const tags = Array.from(tagElements).map(tag => tag.dataset.tag);
|
||||||
|
|
||||||
if (tags.length === 0) {
|
if (tags.length === 0) {
|
||||||
showToast('toast.models.noTagsToAdd', {}, 'warning');
|
showToast('toast.models.noTagsToAdd', {}, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.selectedModels.size === 0) {
|
if (state.selectedModels.size === 0) {
|
||||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const apiClient = getModelApiClient();
|
const apiClient = getModelApiClient();
|
||||||
const filePaths = Array.from(state.selectedModels);
|
const filePaths = Array.from(state.selectedModels);
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let failCount = 0;
|
let failCount = 0;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
state.loadingManager.showSimpleLoading(translate('toast.models.bulkTagsUpdating', { count: filePaths.length }));
|
||||||
|
state.loadingManager.showCancelButton(() => {
|
||||||
|
cancelled = true;
|
||||||
|
});
|
||||||
|
|
||||||
// Add or replace tags for each selected model based on mode
|
// Add or replace tags for each selected model based on mode
|
||||||
for (const filePath of filePaths) {
|
for (const filePath of filePaths) {
|
||||||
|
if (cancelled) {
|
||||||
|
showToast('toast.api.operationCancelled', {}, 'info');
|
||||||
|
break;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (mode === 'replace') {
|
if (mode === 'replace') {
|
||||||
await apiClient.saveModelMetadata(filePath, { tags: tags });
|
await apiClient.saveModelMetadata(filePath, { tags: tags });
|
||||||
@@ -994,50 +1004,50 @@ export class BulkManager {
|
|||||||
failCount++;
|
failCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
modalManager.closeModal('bulkAddTagsModal');
|
modalManager.closeModal('bulkAddTagsModal');
|
||||||
|
|
||||||
if (successCount > 0) {
|
if (successCount > 0) {
|
||||||
const currentConfig = this.getCurrentDisplayConfig();
|
const currentConfig = this.getCurrentDisplayConfig();
|
||||||
const toastKey = mode === 'replace' ? 'toast.models.tagsReplacedSuccessfully' : 'toast.models.tagsAddedSuccessfully';
|
const toastKey = mode === 'replace' ? 'toast.models.tagsReplacedSuccessfully' : 'toast.models.tagsAddedSuccessfully';
|
||||||
showToast(toastKey, {
|
showToast(toastKey, {
|
||||||
count: successCount,
|
count: successCount,
|
||||||
tagCount: tags.length,
|
tagCount: tags.length,
|
||||||
type: currentConfig.displayName.toLowerCase()
|
type: currentConfig.displayName.toLowerCase()
|
||||||
}, 'success');
|
}, 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failCount > 0) {
|
if (failCount > 0) {
|
||||||
const toastKey = mode === 'replace' ? 'toast.models.tagsReplaceFailed' : 'toast.models.tagsAddFailed';
|
const toastKey = mode === 'replace' ? 'toast.models.tagsReplaceFailed' : 'toast.models.tagsAddFailed';
|
||||||
showToast(toastKey, { count: failCount }, 'warning');
|
showToast(toastKey, { count: failCount }, 'warning');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during bulk tag operation:', error);
|
console.error('Error during bulk tag operation:', error);
|
||||||
const toastKey = mode === 'replace' ? 'toast.models.bulkTagsReplaceFailed' : 'toast.models.bulkTagsAddFailed';
|
const toastKey = mode === 'replace' ? 'toast.models.bulkTagsReplaceFailed' : 'toast.models.bulkTagsAddFailed';
|
||||||
showToast(toastKey, {}, 'error');
|
showToast(toastKey, {}, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanupBulkAddTagsModal() {
|
cleanupBulkAddTagsModal() {
|
||||||
// Clear tags container
|
// Clear tags container
|
||||||
const tagsContainer = document.getElementById('bulkTagsItems');
|
const tagsContainer = document.getElementById('bulkTagsItems');
|
||||||
if (tagsContainer) {
|
if (tagsContainer) {
|
||||||
tagsContainer.innerHTML = '';
|
tagsContainer.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear input
|
// Clear input
|
||||||
const input = document.querySelector('.bulk-metadata-input');
|
const input = document.querySelector('.bulk-metadata-input');
|
||||||
if (input) {
|
if (input) {
|
||||||
input.value = '';
|
input.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove event listeners (they will be re-added when modal opens again)
|
// Remove event listeners (they will be re-added when modal opens again)
|
||||||
const appendBtn = document.querySelector('.bulk-append-tags-btn');
|
const appendBtn = document.querySelector('.bulk-append-tags-btn');
|
||||||
if (appendBtn) {
|
if (appendBtn) {
|
||||||
appendBtn.replaceWith(appendBtn.cloneNode(true));
|
appendBtn.replaceWith(appendBtn.cloneNode(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
const replaceBtn = document.querySelector('.bulk-replace-tags-btn');
|
const replaceBtn = document.querySelector('.bulk-replace-tags-btn');
|
||||||
if (replaceBtn) {
|
if (replaceBtn) {
|
||||||
replaceBtn.replaceWith(replaceBtn.cloneNode(true));
|
replaceBtn.replaceWith(replaceBtn.cloneNode(true));
|
||||||
@@ -1140,6 +1150,10 @@ export class BulkManager {
|
|||||||
const levelName = getNSFWLevelName(level);
|
const levelName = getNSFWLevelName(level);
|
||||||
|
|
||||||
state.loadingManager.showSimpleLoading(translate('toast.models.bulkContentRatingUpdating', { count: totalCount }));
|
state.loadingManager.showSimpleLoading(translate('toast.models.bulkContentRatingUpdating', { count: totalCount }));
|
||||||
|
let cancelled = false;
|
||||||
|
state.loadingManager.showCancelButton(() => {
|
||||||
|
cancelled = true;
|
||||||
|
});
|
||||||
|
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let failureCount = 0;
|
let failureCount = 0;
|
||||||
@@ -1147,6 +1161,10 @@ export class BulkManager {
|
|||||||
try {
|
try {
|
||||||
const apiClient = getModelApiClient();
|
const apiClient = getModelApiClient();
|
||||||
for (const filePath of targets) {
|
for (const filePath of targets) {
|
||||||
|
if (cancelled) {
|
||||||
|
showToast('toast.api.operationCancelled', {}, 'info');
|
||||||
|
break;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await apiClient.saveModelMetadata(filePath, { preview_nsfw_level: level });
|
await apiClient.saveModelMetadata(filePath, { preview_nsfw_level: level });
|
||||||
successCount++;
|
successCount++;
|
||||||
@@ -1180,10 +1198,10 @@ export class BulkManager {
|
|||||||
initializeBulkBaseModelInterface() {
|
initializeBulkBaseModelInterface() {
|
||||||
const select = document.getElementById('bulkBaseModelSelect');
|
const select = document.getElementById('bulkBaseModelSelect');
|
||||||
if (!select) return;
|
if (!select) return;
|
||||||
|
|
||||||
// Clear existing options
|
// Clear existing options
|
||||||
select.innerHTML = '';
|
select.innerHTML = '';
|
||||||
|
|
||||||
// Add placeholder option
|
// Add placeholder option
|
||||||
const placeholderOption = document.createElement('option');
|
const placeholderOption = document.createElement('option');
|
||||||
placeholderOption.value = '';
|
placeholderOption.value = '';
|
||||||
@@ -1191,23 +1209,23 @@ export class BulkManager {
|
|||||||
placeholderOption.disabled = true;
|
placeholderOption.disabled = true;
|
||||||
placeholderOption.selected = true;
|
placeholderOption.selected = true;
|
||||||
select.appendChild(placeholderOption);
|
select.appendChild(placeholderOption);
|
||||||
|
|
||||||
// Create option groups for better organization
|
// Create option groups for better organization
|
||||||
Object.entries(BASE_MODEL_CATEGORIES).forEach(([category, models]) => {
|
Object.entries(BASE_MODEL_CATEGORIES).forEach(([category, models]) => {
|
||||||
const optgroup = document.createElement('optgroup');
|
const optgroup = document.createElement('optgroup');
|
||||||
optgroup.label = category;
|
optgroup.label = category;
|
||||||
|
|
||||||
models.forEach(model => {
|
models.forEach(model => {
|
||||||
const option = document.createElement('option');
|
const option = document.createElement('option');
|
||||||
option.value = model;
|
option.value = model;
|
||||||
option.textContent = model;
|
option.textContent = model;
|
||||||
optgroup.appendChild(option);
|
optgroup.appendChild(option);
|
||||||
});
|
});
|
||||||
|
|
||||||
select.appendChild(optgroup);
|
select.appendChild(optgroup);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save bulk base model changes
|
* Save bulk base model changes
|
||||||
*/
|
*/
|
||||||
@@ -1217,25 +1235,33 @@ export class BulkManager {
|
|||||||
showToast('toast.models.baseModelNotSelected', {}, 'warning');
|
showToast('toast.models.baseModelNotSelected', {}, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newBaseModel = select.value;
|
const newBaseModel = select.value;
|
||||||
const selectedCount = state.selectedModels.size;
|
const selectedCount = state.selectedModels.size;
|
||||||
|
|
||||||
if (selectedCount === 0) {
|
if (selectedCount === 0) {
|
||||||
showToast('toast.models.noModelsSelected', {}, 'warning');
|
showToast('toast.models.noModelsSelected', {}, 'warning');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
modalManager.closeModal('bulkBaseModelModal');
|
modalManager.closeModal('bulkBaseModelModal');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let errorCount = 0;
|
let errorCount = 0;
|
||||||
const errors = [];
|
const errors = [];
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
state.loadingManager.showSimpleLoading(translate('toast.models.bulkBaseModelUpdating'));
|
state.loadingManager.showSimpleLoading(translate('toast.models.bulkBaseModelUpdating'));
|
||||||
|
state.loadingManager.showCancelButton(() => {
|
||||||
|
cancelled = true;
|
||||||
|
});
|
||||||
|
|
||||||
for (const filepath of state.selectedModels) {
|
for (const filepath of state.selectedModels) {
|
||||||
|
if (cancelled) {
|
||||||
|
showToast('toast.api.operationCancelled', {}, 'info');
|
||||||
|
break;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await getModelApiClient().saveModelMetadata(filepath, { base_model: newBaseModel });
|
await getModelApiClient().saveModelMetadata(filepath, { base_model: newBaseModel });
|
||||||
successCount++;
|
successCount++;
|
||||||
@@ -1245,19 +1271,19 @@ export class BulkManager {
|
|||||||
console.error(`Failed to update base model for ${filepath}:`, error);
|
console.error(`Failed to update base model for ${filepath}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show results
|
// Show results
|
||||||
if (errorCount === 0) {
|
if (errorCount === 0) {
|
||||||
showToast('toast.models.bulkBaseModelUpdateSuccess', { count: successCount }, 'success');
|
showToast('toast.models.bulkBaseModelUpdateSuccess', { count: successCount }, 'success');
|
||||||
} else if (successCount > 0) {
|
} else if (successCount > 0) {
|
||||||
showToast('toast.models.bulkBaseModelUpdatePartial', {
|
showToast('toast.models.bulkBaseModelUpdatePartial', {
|
||||||
success: successCount,
|
success: successCount,
|
||||||
failed: errorCount
|
failed: errorCount
|
||||||
}, 'warning');
|
}, 'warning');
|
||||||
} else {
|
} else {
|
||||||
showToast('toast.models.bulkBaseModelUpdateFailed', {}, 'error');
|
showToast('toast.models.bulkBaseModelUpdateFailed', {}, 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during bulk base model operation:', error);
|
console.error('Error during bulk base model operation:', error);
|
||||||
showToast('toast.models.bulkBaseModelUpdateFailed', {}, 'error');
|
showToast('toast.models.bulkBaseModelUpdateFailed', {}, 'error');
|
||||||
@@ -1265,7 +1291,7 @@ export class BulkManager {
|
|||||||
state.loadingManager?.hide?.();
|
state.loadingManager?.hide?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cleanup bulk base model modal
|
* Cleanup bulk base model modal
|
||||||
*/
|
*/
|
||||||
@@ -1288,13 +1314,13 @@ export class BulkManager {
|
|||||||
try {
|
try {
|
||||||
// Get selected file paths
|
// Get selected file paths
|
||||||
const filePaths = Array.from(state.selectedModels);
|
const filePaths = Array.from(state.selectedModels);
|
||||||
|
|
||||||
// Get the API client for the current model type
|
// Get the API client for the current model type
|
||||||
const apiClient = getModelApiClient();
|
const apiClient = getModelApiClient();
|
||||||
|
|
||||||
// Call the auto-organize method with selected file paths
|
// Call the auto-organize method with selected file paths
|
||||||
await apiClient.autoOrganizeModels(filePaths);
|
await apiClient.autoOrganizeModels(filePaths);
|
||||||
|
|
||||||
resetAndReload(true);
|
resetAndReload(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error during bulk auto-organize:', error);
|
console.error('Error during bulk auto-organize:', error);
|
||||||
@@ -1310,7 +1336,7 @@ export class BulkManager {
|
|||||||
this.mouseDownTime = Date.now();
|
this.mouseDownTime = Date.now();
|
||||||
this.mouseDownPosition = { x: e.clientX, y: e.clientY };
|
this.mouseDownPosition = { x: e.clientX, y: e.clientY };
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
|
|
||||||
// Don't start marquee yet - wait to see if user is dragging
|
// Don't start marquee yet - wait to see if user is dragging
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1324,23 +1350,23 @@ export class BulkManager {
|
|||||||
// Store initial mouse position
|
// Store initial mouse position
|
||||||
this.marqueeStart.x = this.mouseDownPosition.x;
|
this.marqueeStart.x = this.mouseDownPosition.x;
|
||||||
this.marqueeStart.y = this.mouseDownPosition.y;
|
this.marqueeStart.y = this.mouseDownPosition.y;
|
||||||
|
|
||||||
// Store initial selection state
|
// Store initial selection state
|
||||||
this.initialSelectedModels = new Set(state.selectedModels);
|
this.initialSelectedModels = new Set(state.selectedModels);
|
||||||
|
|
||||||
// Enter bulk mode if not already active and we're actually dragging
|
// Enter bulk mode if not already active and we're actually dragging
|
||||||
if (isDragging && !state.bulkMode) {
|
if (isDragging && !state.bulkMode) {
|
||||||
this.toggleBulkMode();
|
this.toggleBulkMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create marquee element
|
// Create marquee element
|
||||||
this.createMarqueeElement();
|
this.createMarqueeElement();
|
||||||
|
|
||||||
this.isMarqueeActive = true;
|
this.isMarqueeActive = true;
|
||||||
|
|
||||||
// Update event manager state
|
// Update event manager state
|
||||||
eventManager.setState('marqueeActive', true);
|
eventManager.setState('marqueeActive', true);
|
||||||
|
|
||||||
// Add visual feedback class to body
|
// Add visual feedback class to body
|
||||||
document.body.classList.add('marquee-selecting');
|
document.body.classList.add('marquee-selecting');
|
||||||
}
|
}
|
||||||
@@ -1370,22 +1396,22 @@ export class BulkManager {
|
|||||||
*/
|
*/
|
||||||
updateMarqueeSelection(e) {
|
updateMarqueeSelection(e) {
|
||||||
if (!this.marqueeElement) return;
|
if (!this.marqueeElement) return;
|
||||||
|
|
||||||
const currentX = e.clientX;
|
const currentX = e.clientX;
|
||||||
const currentY = e.clientY;
|
const currentY = e.clientY;
|
||||||
|
|
||||||
// Calculate rectangle bounds
|
// Calculate rectangle bounds
|
||||||
const left = Math.min(this.marqueeStart.x, currentX);
|
const left = Math.min(this.marqueeStart.x, currentX);
|
||||||
const top = Math.min(this.marqueeStart.y, currentY);
|
const top = Math.min(this.marqueeStart.y, currentY);
|
||||||
const width = Math.abs(currentX - this.marqueeStart.x);
|
const width = Math.abs(currentX - this.marqueeStart.x);
|
||||||
const height = Math.abs(currentY - this.marqueeStart.y);
|
const height = Math.abs(currentY - this.marqueeStart.y);
|
||||||
|
|
||||||
// Update marquee element position and size
|
// Update marquee element position and size
|
||||||
this.marqueeElement.style.left = left + 'px';
|
this.marqueeElement.style.left = left + 'px';
|
||||||
this.marqueeElement.style.top = top + 'px';
|
this.marqueeElement.style.top = top + 'px';
|
||||||
this.marqueeElement.style.width = width + 'px';
|
this.marqueeElement.style.width = width + 'px';
|
||||||
this.marqueeElement.style.height = height + 'px';
|
this.marqueeElement.style.height = height + 'px';
|
||||||
|
|
||||||
// Check which cards intersect with marquee
|
// Check which cards intersect with marquee
|
||||||
this.updateCardSelection(left, top, left + width, top + height);
|
this.updateCardSelection(left, top, left + width, top + height);
|
||||||
}
|
}
|
||||||
@@ -1396,18 +1422,18 @@ export class BulkManager {
|
|||||||
updateCardSelection(left, top, right, bottom) {
|
updateCardSelection(left, top, right, bottom) {
|
||||||
const cards = document.querySelectorAll('.model-card');
|
const cards = document.querySelectorAll('.model-card');
|
||||||
const newSelection = new Set(this.initialSelectedModels);
|
const newSelection = new Set(this.initialSelectedModels);
|
||||||
|
|
||||||
cards.forEach(card => {
|
cards.forEach(card => {
|
||||||
const rect = card.getBoundingClientRect();
|
const rect = card.getBoundingClientRect();
|
||||||
|
|
||||||
// Check if card intersects with marquee rectangle
|
// Check if card intersects with marquee rectangle
|
||||||
const intersects = !(rect.right < left ||
|
const intersects = !(rect.right < left ||
|
||||||
rect.left > right ||
|
rect.left > right ||
|
||||||
rect.bottom < top ||
|
rect.bottom < top ||
|
||||||
rect.top > bottom);
|
rect.top > bottom);
|
||||||
|
|
||||||
const filepath = card.dataset.filepath;
|
const filepath = card.dataset.filepath;
|
||||||
|
|
||||||
if (intersects) {
|
if (intersects) {
|
||||||
// Add to selection if intersecting
|
// Add to selection if intersecting
|
||||||
newSelection.add(filepath);
|
newSelection.add(filepath);
|
||||||
@@ -1424,10 +1450,10 @@ export class BulkManager {
|
|||||||
card.classList.remove('selected');
|
card.classList.remove('selected');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update global selection state
|
// Update global selection state
|
||||||
state.selectedModels = newSelection;
|
state.selectedModels = newSelection;
|
||||||
|
|
||||||
// Update context menu header if visible
|
// Update context menu header if visible
|
||||||
if (this.bulkContextMenu) {
|
if (this.bulkContextMenu) {
|
||||||
this.bulkContextMenu.updateSelectedCountHeader();
|
this.bulkContextMenu.updateSelectedCountHeader();
|
||||||
@@ -1442,29 +1468,29 @@ export class BulkManager {
|
|||||||
this.isMarqueeActive = false;
|
this.isMarqueeActive = false;
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
this.mouseDownTime = 0;
|
this.mouseDownTime = 0;
|
||||||
|
|
||||||
// Update event manager state
|
// Update event manager state
|
||||||
eventManager.setState('marqueeActive', false);
|
eventManager.setState('marqueeActive', false);
|
||||||
|
|
||||||
// Remove marquee element
|
// Remove marquee element
|
||||||
if (this.marqueeElement) {
|
if (this.marqueeElement) {
|
||||||
this.marqueeElement.remove();
|
this.marqueeElement.remove();
|
||||||
this.marqueeElement = null;
|
this.marqueeElement = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove visual feedback class
|
// Remove visual feedback class
|
||||||
document.body.classList.remove('marquee-selecting');
|
document.body.classList.remove('marquee-selecting');
|
||||||
|
|
||||||
// Get selection count
|
// Get selection count
|
||||||
const selectionCount = state.selectedModels.size;
|
const selectionCount = state.selectedModels.size;
|
||||||
|
|
||||||
// If no models were selected, exit bulk mode
|
// If no models were selected, exit bulk mode
|
||||||
if (selectionCount === 0) {
|
if (selectionCount === 0) {
|
||||||
if (state.bulkMode) {
|
if (state.bulkMode) {
|
||||||
this.toggleBulkMode();
|
this.toggleBulkMode();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear initial selection state
|
// Clear initial selection state
|
||||||
this.initialSelectedModels.clear();
|
this.initialSelectedModels.clear();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,25 @@ export class LoadingManager {
|
|||||||
this.loadingContent.appendChild(this.statusText);
|
this.loadingContent.appendChild(this.statusText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.cancelButton = this.loadingContent.querySelector('.loading-cancel');
|
||||||
|
if (!this.cancelButton) {
|
||||||
|
this.cancelButton = document.createElement('button');
|
||||||
|
this.cancelButton.className = 'loading-cancel secondary-btn';
|
||||||
|
this.cancelButton.style.display = 'none';
|
||||||
|
this.cancelButton.style.margin = 'var(--space-2) auto 0';
|
||||||
|
this.cancelButton.textContent = translate('common.actions.cancel', {}, 'Cancel');
|
||||||
|
this.loadingContent.appendChild(this.cancelButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onCancelCallback = null;
|
||||||
|
this.cancelButton.onclick = () => {
|
||||||
|
if (this.onCancelCallback) {
|
||||||
|
this.onCancelCallback();
|
||||||
|
this.cancelButton.disabled = true;
|
||||||
|
this.cancelButton.textContent = translate('common.status.loading', {}, 'Loading...');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
this.detailsContainer = null; // Will be created when needed
|
this.detailsContainer = null; // Will be created when needed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +65,7 @@ export class LoadingManager {
|
|||||||
this.overlay.style.display = 'flex';
|
this.overlay.style.display = 'flex';
|
||||||
this.setProgress(progress);
|
this.setProgress(progress);
|
||||||
this.setStatus(message);
|
this.setStatus(message);
|
||||||
|
|
||||||
// Remove any existing details container
|
// Remove any existing details container
|
||||||
this.removeDetailsContainer();
|
this.removeDetailsContainer();
|
||||||
}
|
}
|
||||||
@@ -70,26 +89,43 @@ export class LoadingManager {
|
|||||||
this.setProgress(0);
|
this.setProgress(0);
|
||||||
this.setStatus('');
|
this.setStatus('');
|
||||||
this.removeDetailsContainer();
|
this.removeDetailsContainer();
|
||||||
|
this.hideCancelButton();
|
||||||
this.progressBar.style.display = 'block';
|
this.progressBar.style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showCancelButton(onCancel) {
|
||||||
|
if (this.cancelButton) {
|
||||||
|
this.onCancelCallback = onCancel;
|
||||||
|
this.cancelButton.style.display = 'flex';
|
||||||
|
this.cancelButton.disabled = false;
|
||||||
|
this.cancelButton.textContent = translate('common.actions.cancel', {}, 'Cancel');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hideCancelButton() {
|
||||||
|
if (this.cancelButton) {
|
||||||
|
this.cancelButton.style.display = 'none';
|
||||||
|
this.onCancelCallback = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create a details container for enhanced progress display
|
// Create a details container for enhanced progress display
|
||||||
createDetailsContainer() {
|
createDetailsContainer() {
|
||||||
// Remove existing container if any
|
// Remove existing container if any
|
||||||
this.removeDetailsContainer();
|
this.removeDetailsContainer();
|
||||||
|
|
||||||
// Create new container
|
// Create new container
|
||||||
this.detailsContainer = document.createElement('div');
|
this.detailsContainer = document.createElement('div');
|
||||||
this.detailsContainer.className = 'progress-details-container';
|
this.detailsContainer.className = 'progress-details-container';
|
||||||
|
|
||||||
// Insert after the main progress bar
|
// Insert after the main progress bar
|
||||||
if (this.loadingContent) {
|
if (this.loadingContent) {
|
||||||
this.loadingContent.appendChild(this.detailsContainer);
|
this.loadingContent.appendChild(this.detailsContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.detailsContainer;
|
return this.detailsContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove details container
|
// Remove details container
|
||||||
removeDetailsContainer() {
|
removeDetailsContainer() {
|
||||||
if (this.detailsContainer) {
|
if (this.detailsContainer) {
|
||||||
@@ -97,39 +133,39 @@ export class LoadingManager {
|
|||||||
this.detailsContainer = null;
|
this.detailsContainer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show enhanced progress for downloads
|
// Show enhanced progress for downloads
|
||||||
showDownloadProgress(totalItems = 1) {
|
showDownloadProgress(totalItems = 1) {
|
||||||
this.show(translate('modals.download.status.preparing', {}, 'Preparing download...'), 0);
|
this.show(translate('modals.download.status.preparing', {}, 'Preparing download...'), 0);
|
||||||
this.progressBar.style.display = 'none';
|
this.progressBar.style.display = 'none';
|
||||||
|
|
||||||
// Create details container
|
// Create details container
|
||||||
const detailsContainer = this.createDetailsContainer();
|
const detailsContainer = this.createDetailsContainer();
|
||||||
|
|
||||||
// Create current item progress
|
// Create current item progress
|
||||||
const currentItemContainer = document.createElement('div');
|
const currentItemContainer = document.createElement('div');
|
||||||
currentItemContainer.className = 'current-item-progress';
|
currentItemContainer.className = 'current-item-progress';
|
||||||
|
|
||||||
const currentItemLabel = document.createElement('div');
|
const currentItemLabel = document.createElement('div');
|
||||||
currentItemLabel.className = 'current-item-label';
|
currentItemLabel.className = 'current-item-label';
|
||||||
currentItemLabel.textContent = translate('modals.download.progress.currentFile', {}, 'Current file:');
|
currentItemLabel.textContent = translate('modals.download.progress.currentFile', {}, 'Current file:');
|
||||||
|
|
||||||
const currentItemBar = document.createElement('div');
|
const currentItemBar = document.createElement('div');
|
||||||
currentItemBar.className = 'current-item-bar-container';
|
currentItemBar.className = 'current-item-bar-container';
|
||||||
|
|
||||||
const currentItemProgress = document.createElement('div');
|
const currentItemProgress = document.createElement('div');
|
||||||
currentItemProgress.className = 'current-item-bar';
|
currentItemProgress.className = 'current-item-bar';
|
||||||
currentItemProgress.style.width = '0%';
|
currentItemProgress.style.width = '0%';
|
||||||
|
|
||||||
const currentItemPercent = document.createElement('span');
|
const currentItemPercent = document.createElement('span');
|
||||||
currentItemPercent.className = 'current-item-percent';
|
currentItemPercent.className = 'current-item-percent';
|
||||||
currentItemPercent.textContent = '0%';
|
currentItemPercent.textContent = '0%';
|
||||||
|
|
||||||
currentItemBar.appendChild(currentItemProgress);
|
currentItemBar.appendChild(currentItemProgress);
|
||||||
currentItemContainer.appendChild(currentItemLabel);
|
currentItemContainer.appendChild(currentItemLabel);
|
||||||
currentItemContainer.appendChild(currentItemBar);
|
currentItemContainer.appendChild(currentItemBar);
|
||||||
currentItemContainer.appendChild(currentItemPercent);
|
currentItemContainer.appendChild(currentItemPercent);
|
||||||
|
|
||||||
// Create overall progress elements if multiple items
|
// Create overall progress elements if multiple items
|
||||||
let overallLabel = null;
|
let overallLabel = null;
|
||||||
if (totalItems > 1) {
|
if (totalItems > 1) {
|
||||||
@@ -138,7 +174,7 @@ export class LoadingManager {
|
|||||||
overallLabel.textContent = `Overall progress (0/${totalItems} complete):`;
|
overallLabel.textContent = `Overall progress (0/${totalItems} complete):`;
|
||||||
detailsContainer.appendChild(overallLabel);
|
detailsContainer.appendChild(overallLabel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add current item progress to container
|
// Add current item progress to container
|
||||||
detailsContainer.appendChild(currentItemContainer);
|
detailsContainer.appendChild(currentItemContainer);
|
||||||
|
|
||||||
@@ -217,13 +253,13 @@ export class LoadingManager {
|
|||||||
|
|
||||||
// Initialize transfer stats with empty data
|
// Initialize transfer stats with empty data
|
||||||
updateTransferStats();
|
updateTransferStats();
|
||||||
|
|
||||||
// Return update function
|
// Return update function
|
||||||
return (currentProgress, currentIndex = 0, currentName = '', metrics = {}) => {
|
return (currentProgress, currentIndex = 0, currentName = '', metrics = {}) => {
|
||||||
// Update current item progress
|
// Update current item progress
|
||||||
currentItemProgress.style.width = `${currentProgress}%`;
|
currentItemProgress.style.width = `${currentProgress}%`;
|
||||||
currentItemPercent.textContent = `${Math.floor(currentProgress)}%`;
|
currentItemPercent.textContent = `${Math.floor(currentProgress)}%`;
|
||||||
|
|
||||||
// Update current item label if name provided
|
// Update current item label if name provided
|
||||||
if (currentName) {
|
if (currentName) {
|
||||||
currentItemLabel.textContent = translate(
|
currentItemLabel.textContent = translate(
|
||||||
@@ -232,13 +268,13 @@ export class LoadingManager {
|
|||||||
`Downloading: ${currentName}`
|
`Downloading: ${currentName}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update overall label if multiple items
|
// Update overall label if multiple items
|
||||||
if (totalItems > 1 && overallLabel) {
|
if (totalItems > 1 && overallLabel) {
|
||||||
overallLabel.textContent = `Overall progress (${currentIndex}/${totalItems} complete):`;
|
overallLabel.textContent = `Overall progress (${currentIndex}/${totalItems} complete):`;
|
||||||
|
|
||||||
// Calculate and update overall progress
|
// Calculate and update overall progress
|
||||||
const overallProgress = Math.floor((currentIndex + currentProgress/100) / totalItems * 100);
|
const overallProgress = Math.floor((currentIndex + currentProgress / 100) / totalItems * 100);
|
||||||
this.setProgress(overallProgress);
|
this.setProgress(overallProgress);
|
||||||
} else {
|
} else {
|
||||||
// Single item, just update main progress
|
// Single item, just update main progress
|
||||||
@@ -251,7 +287,7 @@ export class LoadingManager {
|
|||||||
|
|
||||||
async showWithProgress(callback, options = {}) {
|
async showWithProgress(callback, options = {}) {
|
||||||
const { initialMessage = 'Processing...', completionMessage = 'Complete' } = options;
|
const { initialMessage = 'Processing...', completionMessage = 'Complete' } = options;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.show(initialMessage);
|
this.show(initialMessage);
|
||||||
await callback(this);
|
await callback(this);
|
||||||
@@ -266,16 +302,20 @@ export class LoadingManager {
|
|||||||
// Enhanced progress display without callback pattern
|
// Enhanced progress display without callback pattern
|
||||||
showEnhancedProgress(message = 'Processing...') {
|
showEnhancedProgress(message = 'Processing...') {
|
||||||
this.show(message, 0);
|
this.show(message, 0);
|
||||||
|
|
||||||
// Return update functions
|
// Return update functions
|
||||||
return {
|
return {
|
||||||
updateProgress: (percent, currentItem = '', statusMessage = '') => {
|
updateProgress: (percent, currentItem = '', statusMessage = '') => {
|
||||||
this.setProgress(percent);
|
this.setProgress(percent);
|
||||||
if (statusMessage) {
|
if (statusMessage) {
|
||||||
this.setStatus(statusMessage);
|
this.setStatus(statusMessage);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showCancelButton: (onCancel) => {
|
||||||
|
this.showCancelButton(onCancel);
|
||||||
|
},
|
||||||
|
|
||||||
complete: async (completionMessage = 'Complete') => {
|
complete: async (completionMessage = 'Complete') => {
|
||||||
this.setProgress(100);
|
this.setProgress(100);
|
||||||
this.setStatus(completionMessage);
|
this.setStatus(completionMessage);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { state } from '../state/index.js';
|
|||||||
import { translate } from './i18nHelpers.js';
|
import { translate } from './i18nHelpers.js';
|
||||||
import { showToast } from './uiHelpers.js';
|
import { showToast } from './uiHelpers.js';
|
||||||
import { getCompleteApiConfig, getCurrentModelType } from '../api/apiConfig.js';
|
import { getCompleteApiConfig, getCurrentModelType } from '../api/apiConfig.js';
|
||||||
import { resetAndReload } from '../api/modelApiFactory.js';
|
import { resetAndReload, getModelApiClient } from '../api/modelApiFactory.js';
|
||||||
import { getStorageItem, setStorageItem } from './storageHelpers.js';
|
import { getStorageItem, setStorageItem } from './storageHelpers.js';
|
||||||
import { modalManager } from '../managers/ModalManager.js';
|
import { modalManager } from '../managers/ModalManager.js';
|
||||||
|
|
||||||
@@ -18,6 +18,7 @@ const CHECK_UPDATES_CONFIRMATION_KEY = 'ack_check_updates_for_all_models';
|
|||||||
export async function performModelUpdateCheck({ onStart, onComplete } = {}) {
|
export async function performModelUpdateCheck({ onStart, onComplete } = {}) {
|
||||||
const modelType = getCurrentModelType();
|
const modelType = getCurrentModelType();
|
||||||
const apiConfig = getCompleteApiConfig(modelType);
|
const apiConfig = getCompleteApiConfig(modelType);
|
||||||
|
const apiClient = getModelApiClient(modelType);
|
||||||
const displayName = apiConfig?.config?.displayName ?? 'Model';
|
const displayName = apiConfig?.config?.displayName ?? 'Model';
|
||||||
|
|
||||||
if (!apiConfig?.endpoints?.refreshUpdates) {
|
if (!apiConfig?.endpoints?.refreshUpdates) {
|
||||||
@@ -41,6 +42,7 @@ export async function performModelUpdateCheck({ onStart, onComplete } = {}) {
|
|||||||
onStart?.({ displayName, loadingMessage });
|
onStart?.({ displayName, loadingMessage });
|
||||||
|
|
||||||
state.loadingManager?.showSimpleLoading?.(loadingMessage);
|
state.loadingManager?.showSimpleLoading?.(loadingMessage);
|
||||||
|
state.loadingManager?.showCancelButton?.(() => apiClient.cancelTask());
|
||||||
|
|
||||||
let status = 'success';
|
let status = 'success';
|
||||||
let records = [];
|
let records = [];
|
||||||
@@ -61,6 +63,10 @@ export async function performModelUpdateCheck({ onStart, onComplete } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok || payload.success !== true) {
|
if (!response.ok || payload.success !== true) {
|
||||||
|
if (payload?.status === 'cancelled') {
|
||||||
|
showToast('toast.api.operationCancelled', {}, 'info');
|
||||||
|
return { status: 'cancelled', displayName, records: [], error: null };
|
||||||
|
}
|
||||||
const errorMessage = payload?.error || response.statusText || 'Unknown error';
|
const errorMessage = payload?.error || response.statusText || 'Unknown error';
|
||||||
throw new Error(errorMessage);
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,6 +168,17 @@ class MockScanner:
|
|||||||
self.updated_models: List[Dict[str, Any]] = []
|
self.updated_models: List[Dict[str, Any]] = []
|
||||||
self.preview_updates: List[Dict[str, Any]] = []
|
self.preview_updates: List[Dict[str, Any]] = []
|
||||||
self.bulk_deleted: List[Sequence[str]] = []
|
self.bulk_deleted: List[Sequence[str]] = []
|
||||||
|
self._cancelled = False
|
||||||
|
self.model_type = "test-model"
|
||||||
|
|
||||||
|
def is_cancelled(self) -> bool:
|
||||||
|
return self._cancelled
|
||||||
|
|
||||||
|
def cancel_task(self) -> None:
|
||||||
|
self._cancelled = True
|
||||||
|
|
||||||
|
def reset_cancellation(self) -> None:
|
||||||
|
self._cancelled = False
|
||||||
|
|
||||||
async def get_cached_data(self, force_refresh: bool = False):
|
async def get_cached_data(self, force_refresh: bool = False):
|
||||||
return self._cache
|
return self._cache
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ from py.services.model_update_service import ModelUpdateRecord, ModelVersionReco
|
|||||||
class DummyScanner:
|
class DummyScanner:
|
||||||
def __init__(self, cache):
|
def __init__(self, cache):
|
||||||
self._cache = cache
|
self._cache = cache
|
||||||
|
self._cancelled = False
|
||||||
|
|
||||||
|
def is_cancelled(self) -> bool:
|
||||||
|
return self._cancelled
|
||||||
|
|
||||||
|
def reset_cancellation(self) -> None:
|
||||||
|
self._cancelled = False
|
||||||
|
|
||||||
async def get_cached_data(self):
|
async def get_cached_data(self):
|
||||||
return self._cache
|
return self._cache
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ from py.services.model_update_service import (
|
|||||||
class DummyScanner:
|
class DummyScanner:
|
||||||
def __init__(self, raw_data):
|
def __init__(self, raw_data):
|
||||||
self._cache = SimpleNamespace(raw_data=raw_data, version_index={})
|
self._cache = SimpleNamespace(raw_data=raw_data, version_index={})
|
||||||
|
self._cancelled = False
|
||||||
|
|
||||||
|
def is_cancelled(self) -> bool:
|
||||||
|
return self._cancelled
|
||||||
|
|
||||||
|
def reset_cancellation(self) -> None:
|
||||||
|
self._cancelled = False
|
||||||
|
|
||||||
async def get_cached_data(self, *args, **kwargs):
|
async def get_cached_data(self, *args, **kwargs):
|
||||||
return self._cache
|
return self._cache
|
||||||
|
|||||||
Reference in New Issue
Block a user