自动缩略图插件审核修改
This commit is contained in:
1 parent
f4e69707c3
commit
d02370d6bd
28 files changed
+2308
-50
No files matched your search
@@ -8,10 +8,40 @@
|
||||
"title": "Spine-Vorschaubilder",
|
||||
"noticeIntro": "Nach dem Import von Spine-Assets (.skel / .json + .atlas + Texturen) erstellt dieses Plugin eine Skelettvorschau für Elemente ohne eigenes Vorschaubild.",
|
||||
"noticeSafety": "Nur neu importierte Elemente werden verarbeitet. Vorhandene Elemente werden weder durchsucht noch geändert, und eigene Vorschaubilder werden nie überschrieben.",
|
||||
"noticeCompound": "Bei Elementen mit zusammengesetzter Endung werden die Felder Name und Typ in Eagle erst nach Bestätigung der aktuellen und neuen Werte geändert. Die Originalwerte werden gespeichert und können wiederhergestellt werden.",
|
||||
"noticeModes": "Animiert verwendet die erste Animation und exportiert WebP. Statisch verwendet immer die Setup-Pose.",
|
||||
"noticeDefault": "Die automatische Erstellung ist standardmäßig ausgeschaltet. Aktiviere sie bei Bedarf unten.",
|
||||
"autoToggle": "Beim Import Vorschaubilder erstellen",
|
||||
"clear": "Protokoll leeren",
|
||||
"restoreNames": "Namen/Typen wiederherstellen",
|
||||
"compoundConfirmTitle": "Änderung zusammengesetzter Endungen bestätigen",
|
||||
"compoundConfirmIntro": "Aufgrund der Besonderheit von Dateien mit zusammengesetzter Endung muss Eagle bei diesen Elementen die Felder Name und Typ ändern, damit die erzeugten Vorschaubilder angezeigt werden können. Die Originalwerte werden gespeichert und können in diesem Plugin wiederhergestellt werden.",
|
||||
"compoundOldValue": "Aktuell",
|
||||
"compoundNewValue": "Neu",
|
||||
"compoundSkip": "Überspringen und fortfahren",
|
||||
"compoundConfirm": "Änderungen anwenden",
|
||||
"compoundConfirmSelected": "Auswahl anwenden ({count})",
|
||||
"compoundSkipped": "{label} übersprungen: Änderung der zusammengesetzten Endung nicht bestätigt",
|
||||
"compoundSourceMismatch": "{label} übersprungen: Eagle-Name {current} stimmt nicht mit Quelldatei {source} überein",
|
||||
"compoundCheckFailed": "Zusammengesetzte Endung für {label} konnte nicht geprüft werden: {error}",
|
||||
"compoundNeedsConfirmation": "{label} übersprungen: Felder wurden nach der Bestätigung geändert; bitte erneut prüfen",
|
||||
"compoundBackupUnavailable": "Zusammengesetzte Endungen können nicht geändert werden, weil die Originalwerte nicht gespeichert werden konnten",
|
||||
"compoundRolledBack": "Vorschaubilderstellung fehlgeschlagen; Name und Typ für {label} wurden wiederhergestellt",
|
||||
"compoundRollbackFailed": "Vorschaubilderstellung fehlgeschlagen und {label} konnte nicht automatisch wiederhergestellt werden: {error}",
|
||||
"restoreEmpty": "Keine gespeicherten Namens-/Typänderungen zum Wiederherstellen",
|
||||
"restoreTitle": "Elemente zum Wiederherstellen auswählen",
|
||||
"restoreIntro": "Für diese Elemente sind Originalwerte gespeichert. Wiederherstellbare Elemente sind standardmäßig alle ausgewählt; vom Benutzer geänderte oder nicht lesbare Elemente können nicht ausgewählt werden.",
|
||||
"restoreCurrent": "Aktuell",
|
||||
"restoreTarget": "Wiederherstellen zu",
|
||||
"restoreStatusReady": "Das Element wurde geändert zu {fields}",
|
||||
"restoreStatusAlreadyOriginal": "Hat bereits den Originalwert; die Bestätigung entfernt nur die Sicherung",
|
||||
"restoreStatusChanged": "Die aktuellen Felder wurden vom Benutzer geändert; wird übersprungen",
|
||||
"restoreStatusMissing": "Element nicht gefunden und kann nicht wiederhergestellt werden",
|
||||
"restoreStatusError": "Aktuelle Werte konnten nicht gelesen werden; Wiederherstellung nicht möglich",
|
||||
"restoreCancel": "Abbrechen",
|
||||
"restoreSelected": "Auswahl wiederherstellen ({count})",
|
||||
"restoreComplete": "Wiederherstellung abgeschlossen: {success} erfolgreich, {skipped} übersprungen, {failed} fehlgeschlagen.",
|
||||
"restoreFailed": "Namen/Typen konnten nicht wiederhergestellt werden: {error}",
|
||||
"modeLabel": "Erstellungsmodus",
|
||||
"modeAnimated": "Animiert (erste Animation)",
|
||||
"modeStatic": "Statisch (Setup-Pose)",
|
||||
|
||||
@@ -8,10 +8,40 @@
|
||||
"title": "Spine Thumbnails",
|
||||
"noticeIntro": "After importing Spine assets (.skel / .json + .atlas + textures), this plugin creates a skeleton preview for items that do not yet have a custom thumbnail.",
|
||||
"noticeSafety": "Only newly imported items are processed. Existing items are not scanned or changed, and custom thumbnails are never overwritten.",
|
||||
"noticeCompound": "Items with compound suffixes have Eagle name/type fields adjusted only after the current and new values are confirmed. Original values are saved and can be restored.",
|
||||
"noticeModes": "Animated uses the first animation and exports WebP. Still always uses the setup pose.",
|
||||
"noticeDefault": "Automatic generation is off by default. Enable it below when needed.",
|
||||
"autoToggle": "Generate thumbnails on import",
|
||||
"clear": "Clear log",
|
||||
"restoreNames": "Restore names/types",
|
||||
"compoundConfirmTitle": "Confirm compound suffix change",
|
||||
"compoundConfirmIntro": "Because compound-suffix files require special handling, Eagle must adjust the name and type fields for these items so the generated thumbnails can be displayed. Original values will be saved and can be restored from this plugin.",
|
||||
"compoundOldValue": "Current",
|
||||
"compoundNewValue": "New",
|
||||
"compoundSkip": "Skip and continue",
|
||||
"compoundConfirm": "Apply changes",
|
||||
"compoundConfirmSelected": "Apply selected ({count})",
|
||||
"compoundSkipped": "Skipped {label}: compound suffix change was not approved",
|
||||
"compoundSourceMismatch": "Skipped {label}: Eagle name {current} does not match source file {source}",
|
||||
"compoundCheckFailed": "Could not check the compound suffix for {label}: {error}",
|
||||
"compoundNeedsConfirmation": "Skipped {label}: fields changed after confirmation; select it again for a new review",
|
||||
"compoundBackupUnavailable": "Compound suffixes cannot be changed because the original values could not be saved",
|
||||
"compoundRolledBack": "Thumbnail generation failed; restored the name and type for {label}",
|
||||
"compoundRollbackFailed": "Thumbnail generation failed and {label} could not be restored automatically: {error}",
|
||||
"restoreEmpty": "There are no saved name/type changes to restore",
|
||||
"restoreTitle": "Select items to restore",
|
||||
"restoreIntro": "These items have saved original values. All restorable items are selected by default; items changed by the user or unavailable for reading cannot be selected.",
|
||||
"restoreCurrent": "Current",
|
||||
"restoreTarget": "Restore to",
|
||||
"restoreStatusReady": "Item was modified to {fields}",
|
||||
"restoreStatusAlreadyOriginal": "Already has the original value; confirming will only remove the backup",
|
||||
"restoreStatusChanged": "Current fields were changed by the user; this item will be skipped",
|
||||
"restoreStatusMissing": "Item not found and cannot be restored",
|
||||
"restoreStatusError": "Current values could not be read and cannot be restored",
|
||||
"restoreCancel": "Cancel",
|
||||
"restoreSelected": "Restore selected ({count})",
|
||||
"restoreComplete": "Restore complete: {success} succeeded, {skipped} skipped, {failed} failed.",
|
||||
"restoreFailed": "Failed to restore names/types: {error}",
|
||||
"modeLabel": "Generation mode",
|
||||
"modeAnimated": "Animated (first animation)",
|
||||
"modeStatic": "Still (setup pose)",
|
||||
|
||||
@@ -8,10 +8,40 @@
|
||||
"title": "Miniaturas de Spine",
|
||||
"noticeIntro": "Después de importar recursos de Spine (.skel / .json + .atlas + texturas), este complemento crea una vista previa del esqueleto para los elementos que aún no tienen una miniatura personalizada.",
|
||||
"noticeSafety": "Solo se procesan los elementos recién importados. No se buscan ni modifican elementos existentes y nunca se sobrescriben las miniaturas personalizadas.",
|
||||
"noticeCompound": "Los campos de nombre y tipo de Eagle solo se ajustan después de confirmar los valores actuales y nuevos de los elementos con extensión compuesta. Los valores originales se guardan y pueden restaurarse.",
|
||||
"noticeModes": "Animado usa la primera animación y exporta WebP. Estático siempre usa la pose de configuración.",
|
||||
"noticeDefault": "La generación automática está desactivada de forma predeterminada. Actívala abajo cuando la necesites.",
|
||||
"autoToggle": "Generar miniaturas al importar",
|
||||
"clear": "Borrar registro",
|
||||
"restoreNames": "Restaurar nombres/tipos",
|
||||
"compoundConfirmTitle": "Confirmar cambio de extensión compuesta",
|
||||
"compoundConfirmIntro": "Debido a la naturaleza especial de los archivos con extensión compuesta, Eagle debe ajustar los campos de nombre y tipo de estos elementos para mostrar las miniaturas generadas. Los valores originales se guardarán y podrán restaurarse desde este complemento.",
|
||||
"compoundOldValue": "Actual",
|
||||
"compoundNewValue": "Nuevo",
|
||||
"compoundSkip": "Omitir y continuar",
|
||||
"compoundConfirm": "Aplicar cambios",
|
||||
"compoundConfirmSelected": "Aplicar seleccionados ({count})",
|
||||
"compoundSkipped": "Se omitió {label}: no se aprobó el cambio de extensión compuesta",
|
||||
"compoundSourceMismatch": "Se omitió {label}: el nombre de Eagle {current} no coincide con el archivo de origen {source}",
|
||||
"compoundCheckFailed": "No se pudo comprobar la extensión compuesta de {label}: {error}",
|
||||
"compoundNeedsConfirmation": "Se omitió {label}: los campos cambiaron después de confirmar; vuelve a revisarlo",
|
||||
"compoundBackupUnavailable": "No se pueden cambiar extensiones compuestas porque no se pudieron guardar los valores originales",
|
||||
"compoundRolledBack": "La generación falló; se restauraron el nombre y el tipo de {label}",
|
||||
"compoundRollbackFailed": "La generación falló y no se pudo restaurar {label} automáticamente: {error}",
|
||||
"restoreEmpty": "No hay cambios de nombre/tipo guardados para restaurar",
|
||||
"restoreTitle": "Seleccionar elementos para restaurar",
|
||||
"restoreIntro": "Estos elementos tienen valores originales guardados. Los elementos restaurables se seleccionan todos de forma predeterminada; los modificados por el usuario o ilegibles no se pueden seleccionar.",
|
||||
"restoreCurrent": "Actual",
|
||||
"restoreTarget": "Restaurar a",
|
||||
"restoreStatusReady": "El elemento se modificó a {fields}",
|
||||
"restoreStatusAlreadyOriginal": "Ya tiene el valor original; al confirmar solo se eliminará la copia de seguridad",
|
||||
"restoreStatusChanged": "Los campos actuales fueron modificados por el usuario y se omitirán",
|
||||
"restoreStatusMissing": "El elemento no existe y no se puede restaurar",
|
||||
"restoreStatusError": "No se pudieron leer los valores actuales y no se puede restaurar",
|
||||
"restoreCancel": "Cancelar",
|
||||
"restoreSelected": "Restaurar seleccionados ({count})",
|
||||
"restoreComplete": "Restauración completada: {success} correctos, {skipped} omitidos, {failed} fallidos.",
|
||||
"restoreFailed": "No se pudieron restaurar nombres/tipos: {error}",
|
||||
"modeLabel": "Modo de generación",
|
||||
"modeAnimated": "Animado (primera animación)",
|
||||
"modeStatic": "Estático (pose de configuración)",
|
||||
|
||||
@@ -8,10 +8,40 @@
|
||||
"title": "Spineサムネイル",
|
||||
"noticeIntro": "Spine アセット(.skel / .json + .atlas + テクスチャ)を読み込んだ後、カスタムサムネイルがないアイテムにスケルトンプレビューを生成します。",
|
||||
"noticeSafety": "今回新しく読み込んだアイテムだけを処理し、既存アイテムの走査や変更、カスタムサムネイルの上書きは行いません。",
|
||||
"noticeCompound": "複合拡張子の項目は、現在値と新しい値を確認した場合にのみ Eagle の名前/種類フィールドを変更します。元の値は保存され、復元できます。",
|
||||
"noticeModes": "アニメーションは最初のアニメーションを WebP に変換します。静止画は常に setup pose を使用します。",
|
||||
"noticeDefault": "自動生成は既定でオフです。必要なときに下で有効にしてください。",
|
||||
"autoToggle": "読み込み時にサムネイルを自動生成",
|
||||
"clear": "ログを消去",
|
||||
"restoreNames": "名前/種類を復元",
|
||||
"compoundConfirmTitle": "複合拡張子の変更を確認",
|
||||
"compoundConfirmIntro": "複合拡張子ファイルの特性上、生成したサムネイルを表示するためには Eagle でこれらの素材の名前と種類フィールドを変更する必要があります。元の値は保存され、このプラグインから復元できます。",
|
||||
"compoundOldValue": "変更前",
|
||||
"compoundNewValue": "変更後",
|
||||
"compoundSkip": "スキップして続行",
|
||||
"compoundConfirm": "変更を適用",
|
||||
"compoundConfirmSelected": "選択項目を適用({count})",
|
||||
"compoundSkipped": "{label} をスキップ:複合拡張子の変更が承認されていません",
|
||||
"compoundSourceMismatch": "{label} をスキップ:Eagle の名前 {current} が元ファイル {source} と一致しません",
|
||||
"compoundCheckFailed": "{label} の複合拡張子を確認できませんでした:{error}",
|
||||
"compoundNeedsConfirmation": "{label} をスキップ:確認後にフィールドが変更されたため、次回もう一度確認してください",
|
||||
"compoundBackupUnavailable": "元の値を保存できないため、複合拡張子を変更できません",
|
||||
"compoundRolledBack": "サムネイル生成に失敗したため、{label} の名前と種類を復元しました",
|
||||
"compoundRollbackFailed": "サムネイル生成に失敗し、{label} を自動復元できませんでした:{error}",
|
||||
"restoreEmpty": "復元できる名前/種類の変更はありません",
|
||||
"restoreTitle": "復元する素材を選択",
|
||||
"restoreIntro": "元の値が保存されている素材です。復元可能な項目は既定ですべて選択されます。後からユーザーが変更した素材や読み取れない素材は選択できません。",
|
||||
"restoreCurrent": "現在",
|
||||
"restoreTarget": "復元後",
|
||||
"restoreStatusReady": "素材は {fields} に変更されています",
|
||||
"restoreStatusAlreadyOriginal": "すでに元の値です。確認するとバックアップのみ削除します",
|
||||
"restoreStatusChanged": "現在のフィールドはユーザーが変更済みのためスキップします",
|
||||
"restoreStatusMissing": "素材が見つからないため復元できません",
|
||||
"restoreStatusError": "現在値を読み取れないため復元できません",
|
||||
"restoreCancel": "キャンセル",
|
||||
"restoreSelected": "選択項目を復元({count})",
|
||||
"restoreComplete": "復元完了:成功 {success}、スキップ {skipped}、失敗 {failed}。",
|
||||
"restoreFailed": "名前/種類の復元に失敗しました:{error}",
|
||||
"modeLabel": "生成方式",
|
||||
"modeAnimated": "アニメーション(最初のアニメーション)",
|
||||
"modeStatic": "静止画(setup pose)",
|
||||
|
||||
@@ -8,10 +8,40 @@
|
||||
"title": "Spine 썸네일",
|
||||
"noticeIntro": "Spine 에셋(.skel / .json + .atlas + 텍스처)을 가져온 뒤 사용자 지정 썸네일이 없는 항목에 스켈레톤 미리보기를 생성합니다.",
|
||||
"noticeSafety": "이번에 새로 가져온 항목만 처리합니다. 기존 항목을 검색하거나 수정하지 않으며 사용자 지정 썸네일을 덮어쓰지 않습니다.",
|
||||
"noticeCompound": "복합 확장자 항목은 현재 값과 새 값을 확인한 경우에만 Eagle 이름/유형 필드를 변경합니다. 원래 값은 저장되며 복원할 수 있습니다.",
|
||||
"noticeModes": "애니메이션은 첫 번째 애니메이션을 WebP로 만듭니다. 정지 이미지는 항상 setup pose를 사용합니다.",
|
||||
"noticeDefault": "자동 생성은 기본적으로 꺼져 있습니다. 필요할 때 아래에서 켜세요.",
|
||||
"autoToggle": "가져올 때 썸네일 자동 생성",
|
||||
"clear": "로그 지우기",
|
||||
"restoreNames": "이름/유형 복원",
|
||||
"compoundConfirmTitle": "복합 확장자 변경 확인",
|
||||
"compoundConfirmIntro": "복합 확장자 파일의 특성상 생성된 썸네일을 표시하려면 Eagle에서 이 항목들의 이름과 유형 필드를 조정해야 합니다. 원래 값은 저장되며 이 플러그인에서 복원할 수 있습니다.",
|
||||
"compoundOldValue": "현재 값",
|
||||
"compoundNewValue": "새 값",
|
||||
"compoundSkip": "건너뛰고 계속",
|
||||
"compoundConfirm": "변경 적용",
|
||||
"compoundConfirmSelected": "선택 항목 적용({count})",
|
||||
"compoundSkipped": "{label} 건너뜀: 복합 확장자 변경이 승인되지 않았습니다",
|
||||
"compoundSourceMismatch": "{label} 건너뜀: Eagle 이름 {current}이(가) 실제 원본 파일 {source}과(와) 일치하지 않습니다",
|
||||
"compoundCheckFailed": "{label}의 복합 확장자를 확인할 수 없습니다: {error}",
|
||||
"compoundNeedsConfirmation": "{label} 건너뜀: 확인 후 필드가 변경되었습니다. 다음에 다시 확인하세요",
|
||||
"compoundBackupUnavailable": "원래 값을 저장할 수 없어 복합 확장자를 변경할 수 없습니다",
|
||||
"compoundRolledBack": "썸네일 생성에 실패하여 {label}의 이름과 유형을 복원했습니다",
|
||||
"compoundRollbackFailed": "썸네일 생성에 실패했고 {label}을(를) 자동 복원할 수 없습니다: {error}",
|
||||
"restoreEmpty": "복원할 이름/유형 변경이 없습니다",
|
||||
"restoreTitle": "복원할 항목 선택",
|
||||
"restoreIntro": "원래 값이 저장된 항목입니다. 복원 가능한 항목은 기본적으로 모두 선택됩니다. 이후 사용자가 변경했거나 읽을 수 없는 항목은 선택할 수 없습니다.",
|
||||
"restoreCurrent": "현재",
|
||||
"restoreTarget": "복원 값",
|
||||
"restoreStatusReady": "항목이 {fields} 값으로 변경되었습니다",
|
||||
"restoreStatusAlreadyOriginal": "이미 원래 값입니다. 확인하면 백업만 정리합니다",
|
||||
"restoreStatusChanged": "현재 필드가 사용자에 의해 변경되어 건너뜁니다",
|
||||
"restoreStatusMissing": "항목을 찾을 수 없어 복원할 수 없습니다",
|
||||
"restoreStatusError": "현재 값을 읽을 수 없어 복원할 수 없습니다",
|
||||
"restoreCancel": "취소",
|
||||
"restoreSelected": "선택 항목 복원({count})",
|
||||
"restoreComplete": "복원 완료: 성공 {success}, 건너뜀 {skipped}, 실패 {failed}.",
|
||||
"restoreFailed": "이름/유형 복원 실패: {error}",
|
||||
"modeLabel": "생성 방식",
|
||||
"modeAnimated": "애니메이션(첫 번째 애니메이션)",
|
||||
"modeStatic": "정지 이미지(setup pose)",
|
||||
|
||||
@@ -8,10 +8,40 @@
|
||||
"title": "Миниатюры Spine",
|
||||
"noticeIntro": "После импорта ресурсов Spine (.skel / .json + .atlas + текстуры) плагин создаёт предпросмотр скелета для элементов без пользовательской миниатюры.",
|
||||
"noticeSafety": "Обрабатываются только новые элементы. Существующие элементы не сканируются и не изменяются, пользовательские миниатюры никогда не перезаписываются.",
|
||||
"noticeCompound": "Поля имени и типа Eagle для элементов с составными расширениями изменяются только после подтверждения текущих и новых значений. Исходные значения сохраняются и могут быть восстановлены.",
|
||||
"noticeModes": "Анимированный режим использует первую анимацию и экспортирует WebP. Статический всегда использует setup pose.",
|
||||
"noticeDefault": "Автоматическое создание по умолчанию отключено. Включите его ниже при необходимости.",
|
||||
"autoToggle": "Создавать миниатюры при импорте",
|
||||
"clear": "Очистить журнал",
|
||||
"restoreNames": "Восстановить имена/типы",
|
||||
"compoundConfirmTitle": "Подтвердить изменение составного расширения",
|
||||
"compoundConfirmIntro": "Из-за особенностей файлов с составными расширениями Eagle должен изменить поля имени и типа этих элементов, чтобы показывать созданные миниатюры. Исходные значения будут сохранены и доступны для восстановления в плагине.",
|
||||
"compoundOldValue": "Сейчас",
|
||||
"compoundNewValue": "Новое",
|
||||
"compoundSkip": "Пропустить и продолжить",
|
||||
"compoundConfirm": "Применить изменения",
|
||||
"compoundConfirmSelected": "Применить выбранные ({count})",
|
||||
"compoundSkipped": "{label} пропущен: изменение составного расширения не подтверждено",
|
||||
"compoundSourceMismatch": "{label} пропущен: имя Eagle {current} не совпадает с исходным файлом {source}",
|
||||
"compoundCheckFailed": "Не удалось проверить составное расширение {label}: {error}",
|
||||
"compoundNeedsConfirmation": "{label} пропущен: поля изменились после подтверждения; проверьте элемент заново",
|
||||
"compoundBackupUnavailable": "Нельзя изменить составные расширения: исходные значения не удалось сохранить",
|
||||
"compoundRolledBack": "Создание миниатюры не удалось; имя и тип {label} восстановлены",
|
||||
"compoundRollbackFailed": "Создание миниатюры не удалось, а {label} не удалось восстановить автоматически: {error}",
|
||||
"restoreEmpty": "Нет сохранённых изменений имён/типов для восстановления",
|
||||
"restoreTitle": "Выберите элементы для восстановления",
|
||||
"restoreIntro": "Для этих элементов сохранены исходные значения. Все доступные элементы выбраны по умолчанию; изменённые пользователем или недоступные элементы выбрать нельзя.",
|
||||
"restoreCurrent": "Сейчас",
|
||||
"restoreTarget": "Восстановить до",
|
||||
"restoreStatusReady": "Элемент изменён на {fields}",
|
||||
"restoreStatusAlreadyOriginal": "Уже имеет исходное значение; подтверждение только удалит резервную копию",
|
||||
"restoreStatusChanged": "Текущие поля изменены пользователем; элемент будет пропущен",
|
||||
"restoreStatusMissing": "Элемент не найден, восстановление невозможно",
|
||||
"restoreStatusError": "Не удалось прочитать текущие значения, восстановление невозможно",
|
||||
"restoreCancel": "Отмена",
|
||||
"restoreSelected": "Восстановить выбранные ({count})",
|
||||
"restoreComplete": "Восстановление завершено: успешно {success}, пропущено {skipped}, ошибок {failed}.",
|
||||
"restoreFailed": "Не удалось восстановить имена/типы: {error}",
|
||||
"modeLabel": "Режим создания",
|
||||
"modeAnimated": "Анимированный (первая анимация)",
|
||||
"modeStatic": "Статический (setup pose)",
|
||||
|
||||
@@ -8,10 +8,40 @@
|
||||
"title": "Spine缩略图",
|
||||
"noticeIntro": "导入 Spine 资产(.skel / .json + .atlas + 贴图)后,本插件会为还没设置缩略图的素材生成一张骨骼预览图。",
|
||||
"noticeSafety": "它只处理本次新导入的素材,不扫描、不修改历史素材,也不会覆盖你已经设置过的自定义缩略图。",
|
||||
"noticeCompound": "复合后缀素材只有在确认原值和新值后,才会调整 Eagle 的名称/类型字段;原值会保存并可恢复。",
|
||||
"noticeModes": "动态取骨骼里的第一个动画压成 WebP;静帧始终使用 setup pose。",
|
||||
"noticeDefault": "自动生成默认关闭,需要你在下面手动启用。",
|
||||
"autoToggle": "导入时自动生成缩略图",
|
||||
"clear": "清空日志",
|
||||
"restoreNames": "恢复名称/类型",
|
||||
"compoundConfirmTitle": "确认修改复合后缀",
|
||||
"compoundConfirmIntro": "由于复合后缀文件的特殊性,为了让这些素材显示生成的缩略图,Eagle 需要调整名称和类型字段。原值会保存,可随时在插件面板恢复。",
|
||||
"compoundOldValue": "原值",
|
||||
"compoundNewValue": "新值",
|
||||
"compoundSkip": "跳过并继续",
|
||||
"compoundConfirm": "确认并应用",
|
||||
"compoundConfirmSelected": "应用选中({count})",
|
||||
"compoundSkipped": "已跳过 {label}:未确认修改复合后缀",
|
||||
"compoundSourceMismatch": "已跳过 {label}:Eagle 名称 {current} 与真实来源文件 {source} 不一致",
|
||||
"compoundCheckFailed": "检查 {label} 的复合后缀失败:{error}",
|
||||
"compoundNeedsConfirmation": "已跳过 {label}:确认后字段又发生变化,请下次重新确认",
|
||||
"compoundBackupUnavailable": "无法修改复合后缀,因为原值备份不可用",
|
||||
"compoundRolledBack": "缩略图生成失败,已恢复 {label} 的名称和类型",
|
||||
"compoundRollbackFailed": "缩略图生成失败且无法自动恢复 {label}:{error}",
|
||||
"restoreEmpty": "没有可恢复的名称/类型修改",
|
||||
"restoreTitle": "选择要恢复的素材",
|
||||
"restoreIntro": "以下是插件保存了原值的素材。可恢复项默认全部勾选;后来被用户修改或无法读取的素材不可选择。",
|
||||
"restoreCurrent": "当前",
|
||||
"restoreTarget": "恢复为",
|
||||
"restoreStatusReady": "素材已被修改为 {fields}",
|
||||
"restoreStatusAlreadyOriginal": "已是原值;确认后仅清理备份",
|
||||
"restoreStatusChanged": "当前字段已被用户修改,将跳过",
|
||||
"restoreStatusMissing": "素材不存在,无法恢复",
|
||||
"restoreStatusError": "无法读取当前值,无法恢复",
|
||||
"restoreCancel": "取消",
|
||||
"restoreSelected": "恢复选中({count})",
|
||||
"restoreComplete": "恢复完成:成功 {success},跳过 {skipped},失败 {failed}。",
|
||||
"restoreFailed": "恢复名称/类型失败:{error}",
|
||||
"modeLabel": "生成方式",
|
||||
"modeAnimated": "动态(第一个动画)",
|
||||
"modeStatic": "静帧(setup pose)",
|
||||
|
||||
@@ -8,10 +8,40 @@
|
||||
"title": "Spine縮圖",
|
||||
"noticeIntro": "匯入 Spine 資產(.skel / .json + .atlas + 貼圖)後,此外掛會為尚未設定縮圖的素材產生骨骼預覽圖。",
|
||||
"noticeSafety": "只處理本次新匯入的素材,不掃描或修改歷史素材,也不會覆寫你已設定的自訂縮圖。",
|
||||
"noticeCompound": "複合副檔名素材只有在確認原始值和新值後,才會調整 Eagle 的名稱/類型欄位;原始值會保存並可還原。",
|
||||
"noticeModes": "動態會取第一個動畫並壓成 WebP;靜態一律使用 setup pose。",
|
||||
"noticeDefault": "自動產生預設為關閉,需要時請在下方手動啟用。",
|
||||
"autoToggle": "匯入時自動產生縮圖",
|
||||
"clear": "清除記錄",
|
||||
"restoreNames": "還原名稱/類型",
|
||||
"compoundConfirmTitle": "確認修改複合副檔名",
|
||||
"compoundConfirmIntro": "由於複合副檔名檔案的特殊性,為了讓這些素材顯示產生的縮圖,Eagle 需要調整名稱和類型欄位。原始值會保存,可隨時在外掛面板還原。",
|
||||
"compoundOldValue": "原始值",
|
||||
"compoundNewValue": "新值",
|
||||
"compoundSkip": "略過並繼續",
|
||||
"compoundConfirm": "確認並套用",
|
||||
"compoundConfirmSelected": "套用選取項目({count})",
|
||||
"compoundSkipped": "已略過 {label}:未確認修改複合副檔名",
|
||||
"compoundSourceMismatch": "已略過 {label}:Eagle 名稱 {current} 與實際來源檔案 {source} 不一致",
|
||||
"compoundCheckFailed": "檢查 {label} 的複合副檔名失敗:{error}",
|
||||
"compoundNeedsConfirmation": "已略過 {label}:確認後欄位又發生變化,請下次重新確認",
|
||||
"compoundBackupUnavailable": "無法修改複合副檔名,因為原始值備份不可用",
|
||||
"compoundRolledBack": "縮圖產生失敗,已還原 {label} 的名稱和類型",
|
||||
"compoundRollbackFailed": "縮圖產生失敗且無法自動還原 {label}:{error}",
|
||||
"restoreEmpty": "沒有可還原的名稱/類型修改",
|
||||
"restoreTitle": "選擇要還原的素材",
|
||||
"restoreIntro": "以下是外掛保存了原始值的素材。可還原項目預設全部勾選;後來被使用者修改或無法讀取的素材不可選擇。",
|
||||
"restoreCurrent": "目前",
|
||||
"restoreTarget": "還原為",
|
||||
"restoreStatusReady": "素材已被修改為 {fields}",
|
||||
"restoreStatusAlreadyOriginal": "已是原始值;確認後僅清除備份",
|
||||
"restoreStatusChanged": "目前欄位已被使用者修改,將略過",
|
||||
"restoreStatusMissing": "素材不存在,無法還原",
|
||||
"restoreStatusError": "無法讀取目前值,無法還原",
|
||||
"restoreCancel": "取消",
|
||||
"restoreSelected": "還原選取項目({count})",
|
||||
"restoreComplete": "還原完成:成功 {success},略過 {skipped},失敗 {failed}。",
|
||||
"restoreFailed": "還原名稱/類型失敗:{error}",
|
||||
"modeLabel": "產生方式",
|
||||
"modeAnimated": "動態(第一個動畫)",
|
||||
"modeStatic": "靜態(setup pose)",
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
/* 窗口窄的时候整组换行,而不是把标签压成一列一个字符。 */
|
||||
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; }
|
||||
.toolbar-group { display: flex; flex-wrap: wrap; gap: 8px; flex: 0 0 auto; }
|
||||
#clear { background: var(--panel); color: var(--text); border: 1px solid var(--border);
|
||||
#clear, #restoreNames { background: var(--panel); color: var(--text); border: 1px solid var(--border);
|
||||
border-radius: 6px; padding: 9px 12px; font: inherit; cursor: pointer; white-space: nowrap; }
|
||||
#clear:hover { border-color: var(--accent); color: var(--accent); }
|
||||
#clear:hover, #restoreNames:hover { border-color: var(--accent); color: var(--accent); }
|
||||
.mode-row { display: flex; align-items: center; gap: 8px; color: var(--dim); white-space: nowrap;
|
||||
flex: 0 1 auto;
|
||||
background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 9px 11px; }
|
||||
@@ -51,6 +51,41 @@
|
||||
.filters button { background: var(--panel); color: var(--dim); border: 1px solid var(--border);
|
||||
border-radius: 999px; padding: 3px 12px; font: inherit; font-size: 12px; cursor: pointer; }
|
||||
.filters button.active { color: var(--accent); border-color: var(--accent); }
|
||||
.compat-dialog { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center;
|
||||
padding: 18px; background: rgba(0, 0, 0, .62); }
|
||||
.compat-dialog.hidden { display: none; }
|
||||
.compat-card { width: min(640px, 100%); max-height: calc(100vh - 36px); display: flex;
|
||||
flex-direction: column; gap: 12px; padding: 18px; background: var(--panel);
|
||||
border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 18px 54px rgba(0, 0, 0, .48); }
|
||||
.compat-card h2 { margin: 0; font-size: 16px; }
|
||||
.compat-card p { margin: 0; color: var(--dim); line-height: 1.65; }
|
||||
.compat-list { overflow: auto; border: 1px solid var(--border); border-radius: 6px; }
|
||||
.compat-row { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 9px;
|
||||
padding: 9px 11px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
||||
.compat-row:last-child { border-bottom: 0; }
|
||||
.compat-row input { margin: 3px 0 0; accent-color: var(--accent); }
|
||||
.compat-row-content { min-width: 0; }
|
||||
.compat-file { color: var(--text); font-weight: 500; word-break: break-all; }
|
||||
.compat-path { color: var(--dim); font-size: 12px; word-break: break-all; }
|
||||
.compat-change { margin-top: 4px; font-family: ui-monospace, Consolas, monospace; font-size: 12px; word-break: break-all; }
|
||||
.compat-change code { color: var(--accent); background: transparent; border: 0; padding: 0; }
|
||||
.compat-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||||
.compat-actions button { background: var(--panel); color: var(--text); border: 1px solid var(--border);
|
||||
border-radius: 6px; padding: 7px 14px; font: inherit; cursor: pointer; }
|
||||
.compat-actions button.primary { color: #fff; background: #1976d2; border-color: #1976d2; }
|
||||
.compat-actions button:disabled { opacity: .45; cursor: default; }
|
||||
.restore-row { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 9px;
|
||||
padding: 9px 11px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
||||
.restore-row:last-child { border-bottom: 0; }
|
||||
.restore-row.disabled { opacity: .55; cursor: default; }
|
||||
.restore-row input { margin: 3px 0 0; accent-color: var(--accent); }
|
||||
.restore-current, .restore-target { margin-top: 4px; font-family: ui-monospace, Consolas, monospace;
|
||||
font-size: 12px; word-break: break-all; }
|
||||
.restore-current code, .restore-target code { color: var(--accent); background: transparent; border: 0; padding: 0; }
|
||||
.restore-status { color: var(--dim); font-size: 12px; margin-top: 3px; }
|
||||
.restore-status.applied { color: #3fb950; }
|
||||
.restore-row.disabled .restore-status { color: var(--warn); }
|
||||
.restore-row.disabled .restore-status.applied { color: #3fb950; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -58,6 +93,7 @@
|
||||
<div class="notice">
|
||||
<p data-i18n="thumb.noticeIntro">导入 Spine 资产(.skel / .json + .atlas + 贴图)后,本插件会为还没设置缩略图的素材生成一张骨骼预览图。</p>
|
||||
<p data-i18n="thumb.noticeSafety">它只处理本次新导入的素材,不扫描、不修改历史素材,也不会覆盖你已经设置过的自定义缩略图。</p>
|
||||
<p data-i18n="thumb.noticeCompound">复合后缀素材只有在确认原值和新值后,才会调整 Eagle 的名称/类型字段;原值会保存并可恢复。</p>
|
||||
<p data-i18n="thumb.noticeModes">动态取骨骼里的第一个动画压成 WebP;静帧始终使用 setup pose。</p>
|
||||
<p data-i18n="thumb.noticeDefault">自动生成默认关闭,需要你在下面手动启用。</p>
|
||||
</div>
|
||||
@@ -65,6 +101,7 @@
|
||||
<div class="toolbar-group">
|
||||
<label class="toggle-row"><input type="checkbox" id="toggle"><span data-i18n="thumb.autoToggle">导入时自动生成缩略图</span></label>
|
||||
<button id="clear" type="button" data-i18n="thumb.clear">清空日志</button>
|
||||
<button id="restoreNames" type="button" data-i18n="thumb.restoreNames">恢复名称/类型</button>
|
||||
</div>
|
||||
<div class="toolbar-group">
|
||||
<label class="mode-row"><span data-i18n="thumb.modeLabel">生成方式</span>
|
||||
@@ -92,6 +129,28 @@
|
||||
<button type="button" data-filter="error" data-i18n="thumb.filterError">错误</button>
|
||||
</div>
|
||||
<ul id="log"></ul>
|
||||
<div id="compatDialog" class="compat-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="compatTitle">
|
||||
<div class="compat-card">
|
||||
<h2 id="compatTitle" data-i18n="thumb.compoundConfirmTitle">确认修改复合后缀</h2>
|
||||
<p data-i18n="thumb.compoundConfirmIntro">由于复合后缀文件的特殊性,为了让这些素材显示生成的缩略图,Eagle 需要调整名称和类型字段。原值会保存,可随时在插件面板恢复。</p>
|
||||
<div id="compatList" class="compat-list"></div>
|
||||
<div class="compat-actions">
|
||||
<button id="compatSkip" type="button" data-i18n="thumb.compoundSkip">跳过并继续</button>
|
||||
<button id="compatConfirm" class="primary" type="button" data-i18n="thumb.compoundConfirm">确认并应用</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="restoreDialog" class="compat-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="restoreTitle">
|
||||
<div class="compat-card">
|
||||
<h2 id="restoreTitle" data-i18n="thumb.restoreTitle">选择要恢复的素材</h2>
|
||||
<p data-i18n="thumb.restoreIntro">以下是插件保存了原值的素材。可恢复项默认全部勾选;后来被用户修改或无法读取的素材不可选择。</p>
|
||||
<div id="restoreList" class="compat-list"></div>
|
||||
<div class="compat-actions">
|
||||
<button id="restoreCancel" type="button" data-i18n="thumb.restoreCancel">取消</button>
|
||||
<button id="restoreApply" class="primary" type="button" disabled>恢复选中</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// service 页面里的 require 不以插件目录为基准,本地资源要用绝对路径,这里先把根目录算出来。
|
||||
(function () {
|
||||
@@ -132,6 +191,8 @@
|
||||
<script src="js/spine-legacy-binary.js"></script>
|
||||
<script src="js/spine-badge.js"></script>
|
||||
<script src="js/settings.js"></script>
|
||||
<script src="js/compound-compat.js"></script>
|
||||
<script src="js/instance-coordinator.js"></script>
|
||||
<script src="js/progress-window.js"></script>
|
||||
<script src="js/renderer.js"></script>
|
||||
<script src="js/library-watcher.js"></script>
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
// 复合后缀兼容修改:只生成计划并持久化原值;真正修改 Eagle 字段由插件在用户确认后执行。
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const STORE_VERSION = 1;
|
||||
|
||||
function fullName(name, ext) {
|
||||
return String(name || '') + (ext ? '.' + ext : '');
|
||||
}
|
||||
|
||||
/** 返回需要展示给用户的字段变更;不需要修改或被安全条件阻断时返回 null / blocked。 */
|
||||
function planChange(item, eagleItem, splitCompoundName) {
|
||||
const sourceName = String((item && item.entryName) || '');
|
||||
const currentName = String((eagleItem && eagleItem.name) || '');
|
||||
const currentExt = String((eagleItem && eagleItem.ext) || '');
|
||||
const currentFileName = fullName(currentName, currentExt);
|
||||
if (!sourceName || currentFileName !== sourceName) {
|
||||
return {
|
||||
blocked: true,
|
||||
reason: 'sourceMismatch',
|
||||
sourceName,
|
||||
currentName,
|
||||
currentExt,
|
||||
currentFileName,
|
||||
};
|
||||
}
|
||||
const parts = splitCompoundName(sourceName);
|
||||
if (!parts || (currentName === parts.name && currentExt === parts.ext)) return null;
|
||||
if (fullName(parts.name, parts.ext) !== sourceName) {
|
||||
return {
|
||||
blocked: true,
|
||||
reason: 'sourceMismatch',
|
||||
sourceName,
|
||||
currentName,
|
||||
currentExt,
|
||||
currentFileName,
|
||||
};
|
||||
}
|
||||
return {
|
||||
sourceName,
|
||||
currentName,
|
||||
currentExt,
|
||||
currentFileName,
|
||||
newName: parts.name,
|
||||
newExt: parts.ext,
|
||||
newFileName: fullName(parts.name, parts.ext),
|
||||
};
|
||||
}
|
||||
|
||||
function emptyStore() {
|
||||
return { version: STORE_VERSION, entries: {} };
|
||||
}
|
||||
|
||||
function readStore(file) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
if (!data || data.version !== STORE_VERSION || !data.entries || typeof data.entries !== 'object') {
|
||||
throw new Error('备份文件格式不正确');
|
||||
}
|
||||
return data;
|
||||
} catch (error) {
|
||||
if (error && error.code === 'ENOENT') return emptyStore();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function writeStore(file, data) {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
const temp = file + '.' + process.pid + '.tmp';
|
||||
fs.writeFileSync(temp, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
||||
fs.renameSync(temp, file);
|
||||
}
|
||||
|
||||
function createStore(userDataPath) {
|
||||
const file = path.join(userDataPath, 'SpineThumbnailGenerateServer', 'compound-name-backups.json');
|
||||
return {
|
||||
file,
|
||||
list() {
|
||||
return Object.values(readStore(file).entries)
|
||||
.sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0));
|
||||
},
|
||||
remember(change) {
|
||||
const data = readStore(file);
|
||||
if (!data.entries[change.itemId]) {
|
||||
data.entries[change.itemId] = {
|
||||
itemId: change.itemId,
|
||||
sourceName: change.sourceName,
|
||||
folderPath: change.folderPath || '',
|
||||
originalName: change.currentName,
|
||||
originalExt: change.currentExt,
|
||||
appliedName: change.newName,
|
||||
appliedExt: change.newExt,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
writeStore(file, data);
|
||||
}
|
||||
return data.entries[change.itemId];
|
||||
},
|
||||
remove(itemId) {
|
||||
const data = readStore(file);
|
||||
if (!data.entries[itemId]) return;
|
||||
delete data.entries[itemId];
|
||||
writeStore(file, data);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 为恢复对话框生成可审计列表,并标出哪些条目允许恢复。
|
||||
* 用户后来亲自改过的字段不会被覆盖。
|
||||
*/
|
||||
async function inspectBackups(store, getItem, sourceExists) {
|
||||
const entries = store.list();
|
||||
const rows = [];
|
||||
for (const entry of entries) {
|
||||
let item = null;
|
||||
try {
|
||||
item = await getItem(entry.itemId);
|
||||
if (!item) {
|
||||
let sourcePresent = false;
|
||||
try {
|
||||
sourcePresent = sourceExists ? await sourceExists(entry) : false;
|
||||
} catch (error) { /* 检查失败时保守按缺失处理 */ }
|
||||
if (sourcePresent) {
|
||||
rows.push({
|
||||
entry,
|
||||
item: null,
|
||||
currentName: entry.appliedName,
|
||||
currentExt: entry.appliedExt,
|
||||
status: 'pluginApplied',
|
||||
});
|
||||
} else {
|
||||
rows.push({ entry, item: null, status: 'missing' });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const currentName = String(item.name || '');
|
||||
const currentExt = String(item.ext || '');
|
||||
if (currentName === entry.originalName && currentExt === entry.originalExt) {
|
||||
rows.push({ entry, item, currentName, currentExt, status: 'alreadyOriginal' });
|
||||
continue;
|
||||
}
|
||||
if (currentName !== entry.appliedName || currentExt !== entry.appliedExt) {
|
||||
rows.push({ entry, item, currentName, currentExt, status: 'changed' });
|
||||
continue;
|
||||
}
|
||||
rows.push({ entry, item, currentName, currentExt, status: 'ready' });
|
||||
} catch (error) {
|
||||
rows.push({ entry, item, status: 'error' });
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function restoreBackups(store, selectedItemIds, getItem, saveItem) {
|
||||
const selected = new Set(selectedItemIds || []);
|
||||
const entries = store.list().filter((entry) => selected.has(entry.itemId));
|
||||
const result = { total: entries.length, success: 0, skipped: 0, failed: 0 };
|
||||
for (const entry of entries) {
|
||||
let item = null;
|
||||
try {
|
||||
item = await getItem(entry.itemId);
|
||||
if (!item) {
|
||||
result.skipped += 1;
|
||||
continue;
|
||||
}
|
||||
const currentName = String(item.name || '');
|
||||
const currentExt = String(item.ext || '');
|
||||
if (currentName === entry.originalName && currentExt === entry.originalExt) {
|
||||
store.remove(entry.itemId);
|
||||
result.success += 1;
|
||||
continue;
|
||||
}
|
||||
if (currentName !== entry.appliedName || currentExt !== entry.appliedExt) {
|
||||
result.skipped += 1;
|
||||
continue;
|
||||
}
|
||||
item.name = entry.originalName;
|
||||
item.ext = entry.originalExt;
|
||||
await saveItem(item);
|
||||
store.remove(entry.itemId);
|
||||
result.success += 1;
|
||||
} catch (error) {
|
||||
result.failed += 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const api = { fullName, planChange, createStore, inspectBackups, restoreBackups, STORE_VERSION };
|
||||
if (typeof window !== 'undefined') window.SpineThumbCompoundCompat = api;
|
||||
else if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
||||
|
||||
})();
|
||||
@@ -0,0 +1,127 @@
|
||||
// 多个 Eagle 插件窗口共用 localStorage,用它协调唯一的活动监听实例。
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const KEY = 'spine-thumbnail-active-instance';
|
||||
const STALE_MS = 3500;
|
||||
|
||||
function createFileStorage(file) {
|
||||
return {
|
||||
file,
|
||||
getItem(key) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
return typeof data[key] === 'string' ? data[key] : null;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
setItem(key, value) {
|
||||
let data = {};
|
||||
try {
|
||||
const existing = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
if (existing && typeof existing === 'object') data = existing;
|
||||
} catch (error) { /* first write or damaged file */ }
|
||||
data[key] = String(value);
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
const temp = file + '.' + process.pid + '.' + Date.now() + '.' + Math.random().toString(16).slice(2) + '.tmp';
|
||||
fs.writeFileSync(temp, JSON.stringify(data), 'utf8');
|
||||
fs.renameSync(temp, file);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function readState(storage) {
|
||||
try {
|
||||
const state = JSON.parse(storage.getItem(KEY));
|
||||
if (!state || typeof state.id !== 'string' || !Number.isFinite(state.updatedAt)) return null;
|
||||
return state;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeState(storage, id, visible, now) {
|
||||
const state = { id, visible, updatedAt: now };
|
||||
try { storage.setItem(KEY, JSON.stringify(state)); } catch (error) { /* 无本地存储时仍可运行 */ }
|
||||
return state;
|
||||
}
|
||||
|
||||
function createCoordinator(options) {
|
||||
const storage = options.storage;
|
||||
const id = options.id;
|
||||
const visible = options.isVisible || (() => true);
|
||||
const hide = options.hide || (() => {});
|
||||
const now = options.now || Date.now;
|
||||
const staleMs = options.staleMs || STALE_MS;
|
||||
const intervalMs = options.intervalMs || 1000;
|
||||
let active = false;
|
||||
let started = false;
|
||||
let hiddenForDuplicate = false;
|
||||
let timer = null;
|
||||
|
||||
function claim() {
|
||||
writeState(storage, id, visible(), now());
|
||||
hiddenForDuplicate = false;
|
||||
if (!active) {
|
||||
active = true;
|
||||
if (options.onActiveChanged) options.onActiveChanged(true);
|
||||
}
|
||||
}
|
||||
|
||||
function evaluate() {
|
||||
const state = readState(storage);
|
||||
const currentVisible = visible();
|
||||
const canClaim = !state ||
|
||||
state.id === id ||
|
||||
now() - state.updatedAt > staleMs;
|
||||
const nextActive = canClaim;
|
||||
if (nextActive) {
|
||||
claim();
|
||||
} else if (currentVisible && !hiddenForDuplicate) {
|
||||
hiddenForDuplicate = true;
|
||||
try { hide(); } catch (error) { /* 隐藏失败不影响主窗口 */ }
|
||||
}
|
||||
if (nextActive !== active) {
|
||||
active = nextActive;
|
||||
if (options.onActiveChanged) options.onActiveChanged(active);
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
function handleVisibility() {
|
||||
if (visible()) claim();
|
||||
else evaluate();
|
||||
}
|
||||
|
||||
function start() {
|
||||
if (started) return active;
|
||||
started = true;
|
||||
const state = readState(storage);
|
||||
const canStart = visible() ||
|
||||
!state ||
|
||||
state.id === id ||
|
||||
now() - state.updatedAt > staleMs;
|
||||
if (canStart) claim();
|
||||
else evaluate();
|
||||
timer = setInterval(evaluate, intervalMs);
|
||||
return active;
|
||||
}
|
||||
|
||||
function stop() {
|
||||
if (timer) clearInterval(timer);
|
||||
timer = null;
|
||||
started = false;
|
||||
}
|
||||
|
||||
return { start, stop, evaluate, handleVisibility, isActive: () => active };
|
||||
}
|
||||
|
||||
const api = { createCoordinator, createFileStorage, KEY, STALE_MS };
|
||||
if (typeof window !== 'undefined') window.SpineThumbInstanceCoordinator = api;
|
||||
else if (typeof module !== 'undefined' && module.exports) module.exports = api;
|
||||
|
||||
})();
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
// 每次改动递增。Eagle 里的 service 插件是启动时加载并常驻的,改完代码必须重新加载插件
|
||||
// 才会生效;状态栏会显示这个版本号,方便确认加载的到底是不是最新代码。
|
||||
const VERSION = 'v68';
|
||||
const VERSION = 'v72';
|
||||
const SHOW_DEV_VERSION = false;
|
||||
|
||||
const path = require('path');
|
||||
@@ -16,7 +16,7 @@ const REQUIRED = [
|
||||
'SpineThumbI18n',
|
||||
'SpineThumbLibraryWatcher', 'SpineThumbSpineAssets', 'SpineThumbRenderer', 'SpineThumbSettings',
|
||||
'SpineUtils', 'SpineBadge', 'SpineVisibleBounds', 'SpineAnimatedProfiles', 'SpineAnimatedWebpEncoder',
|
||||
'SpineThumbProgress',
|
||||
'SpineThumbProgress', 'SpineThumbCompoundCompat', 'SpineThumbInstanceCoordinator',
|
||||
];
|
||||
const missing = REQUIRED.filter((name) => !window[name]);
|
||||
if (missing.length) {
|
||||
@@ -30,6 +30,8 @@ const { renderThumbnailAuto, withTimeout } = window.SpineThumbRenderer;
|
||||
const settingsStore = window.SpineThumbSettings;
|
||||
const utils = window.SpineUtils;
|
||||
const progress = window.SpineThumbProgress;
|
||||
const compoundCompat = window.SpineThumbCompoundCompat;
|
||||
const instanceCoordinatorModule = window.SpineThumbInstanceCoordinator;
|
||||
|
||||
const statusEl = document.getElementById('status');
|
||||
const libraryEl = document.getElementById('library');
|
||||
@@ -37,9 +39,18 @@ const statsEl = document.getElementById('stats');
|
||||
const logEl = document.getElementById('log');
|
||||
const toggleEl = document.getElementById('toggle');
|
||||
const clearEl = document.getElementById('clear');
|
||||
const restoreNamesEl = document.getElementById('restoreNames');
|
||||
const filtersEl = document.getElementById('filters');
|
||||
const modeEl = document.getElementById('mode');
|
||||
const strategyEl = document.getElementById('strategy');
|
||||
const compatDialogEl = document.getElementById('compatDialog');
|
||||
const compatListEl = document.getElementById('compatList');
|
||||
const compatConfirmEl = document.getElementById('compatConfirm');
|
||||
const compatSkipEl = document.getElementById('compatSkip');
|
||||
const restoreDialogEl = document.getElementById('restoreDialog');
|
||||
const restoreListEl = document.getElementById('restoreList');
|
||||
const restoreCancelEl = document.getElementById('restoreCancel');
|
||||
const restoreApplyEl = document.getElementById('restoreApply');
|
||||
|
||||
const stats = { batches: 0, added: 0, spine: 0, complete: 0, generated: 0, skipped: 0, failed: 0 };
|
||||
// 一次排队超过这个数量(动态模式)才开进度弹窗(这种批量通常要等很久)。
|
||||
@@ -49,6 +60,9 @@ const PROGRESS_MIN_DYNAMIC = 10;
|
||||
const CONTROL_FILE = path.join(os.tmpdir(), 'spine-thumbnail-progress-control.json');
|
||||
const STALE_TEMP_AGE_MS = 24 * 60 * 60 * 1000;
|
||||
let settings = { ...settingsStore.DEFAULTS };
|
||||
let compatStore = null;
|
||||
let instanceCoordinator = null;
|
||||
let pluginUserDataPath = null;
|
||||
let libraryPath = null;
|
||||
let imagesDir = null;
|
||||
const queue = [];
|
||||
@@ -114,10 +128,19 @@ function loadFolderPaths() {
|
||||
} catch (error) { /* 读不到就只显示未分类 */ }
|
||||
}
|
||||
|
||||
function folderPathOf(folderIds) {
|
||||
function optionalFolderPathOf(folderIds) {
|
||||
if (!folderPaths) loadFolderPaths();
|
||||
const id = (Array.isArray(folderIds) && folderIds[0]) || null;
|
||||
return (id && folderPaths.get(id)) || t('thumb.uncategorized');
|
||||
return (id && folderPaths.get(id)) || '';
|
||||
}
|
||||
|
||||
function folderPathOf(folderIds) {
|
||||
return optionalFolderPathOf(folderIds) || t('thumb.uncategorized');
|
||||
}
|
||||
|
||||
function backupSourceExists(entry) {
|
||||
if (!imagesDir || !entry.sourceName) return false;
|
||||
return fs.existsSync(path.join(imagesDir, entry.itemId + '.info', entry.sourceName));
|
||||
}
|
||||
|
||||
/** 日志里的渲染结果描述。 */
|
||||
@@ -161,9 +184,247 @@ function countRun(outcome, animated, name) {
|
||||
}
|
||||
}
|
||||
|
||||
/** 一个素材的缩略图生成流程:查重 → 渲染 → 写临时文件 → 走官方接口设置 → 清理。 */
|
||||
function compoundMatchesApproval(plan, approved) {
|
||||
return plan && !plan.blocked &&
|
||||
plan.currentName === approved.currentName &&
|
||||
plan.currentExt === approved.currentExt &&
|
||||
plan.newName === approved.newName &&
|
||||
plan.newExt === approved.newExt;
|
||||
}
|
||||
|
||||
function showCompoundConfirmation(changes) {
|
||||
compatListEl.textContent = '';
|
||||
const checkboxes = [];
|
||||
const updateConfirm = () => {
|
||||
const count = checkboxes.filter((input) => input.checked).length;
|
||||
compatConfirmEl.disabled = count === 0;
|
||||
compatConfirmEl.textContent = t('thumb.compoundConfirmSelected', { count });
|
||||
};
|
||||
for (const change of changes) {
|
||||
const plan = change.plan;
|
||||
const row = document.createElement('label');
|
||||
row.className = 'compat-row';
|
||||
const input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.checked = true;
|
||||
input.dataset.itemId = itemIdOf(change.item.dirName);
|
||||
input.addEventListener('change', updateConfirm);
|
||||
checkboxes.push(input);
|
||||
const content = document.createElement('div');
|
||||
content.className = 'compat-row-content';
|
||||
const file = document.createElement('div');
|
||||
file.className = 'compat-file';
|
||||
file.textContent = plan.sourceName;
|
||||
const location = document.createElement('div');
|
||||
location.className = 'compat-path';
|
||||
location.textContent = optionalFolderPathOf(change.item.folders);
|
||||
const values = document.createElement('div');
|
||||
values.className = 'compat-change';
|
||||
const oldValue = document.createElement('code');
|
||||
oldValue.textContent = 'name=' + JSON.stringify(plan.currentName) + '; ext=' + JSON.stringify(plan.currentExt);
|
||||
const newValue = document.createElement('code');
|
||||
newValue.textContent = 'name=' + JSON.stringify(plan.newName) + '; ext=' + JSON.stringify(plan.newExt);
|
||||
values.append(
|
||||
t('thumb.compoundOldValue') + ' ', oldValue,
|
||||
' → ', t('thumb.compoundNewValue') + ' ', newValue,
|
||||
);
|
||||
content.append(file);
|
||||
if (location.textContent) content.append(location);
|
||||
content.append(values);
|
||||
row.append(input, content);
|
||||
compatListEl.appendChild(row);
|
||||
}
|
||||
updateConfirm();
|
||||
compatDialogEl.classList.remove('hidden');
|
||||
try {
|
||||
if (eagle.window && eagle.window.show) {
|
||||
Promise.resolve(eagle.window.show())
|
||||
.then(() => eagle.window.focus && eagle.window.focus())
|
||||
.catch(() => {});
|
||||
}
|
||||
} catch (error) { /* 窗口显示失败时仍可处理后续批次 */ }
|
||||
return new Promise((resolve) => {
|
||||
const finish = (selected) => {
|
||||
compatDialogEl.classList.add('hidden');
|
||||
compatConfirmEl.removeEventListener('click', confirm);
|
||||
compatSkipEl.removeEventListener('click', skip);
|
||||
document.removeEventListener('keydown', keydown);
|
||||
resolve(selected);
|
||||
};
|
||||
const confirm = () => finish(checkboxes
|
||||
.filter((input) => input.checked)
|
||||
.map((input) => input.dataset.itemId));
|
||||
const skip = () => finish([]);
|
||||
const keydown = (event) => {
|
||||
if (event.key === 'Escape') skip();
|
||||
};
|
||||
compatConfirmEl.addEventListener('click', confirm);
|
||||
compatSkipEl.addEventListener('click', skip);
|
||||
document.addEventListener('keydown', keydown);
|
||||
checkboxes[0].focus();
|
||||
});
|
||||
}
|
||||
|
||||
function fieldsText(name, ext) {
|
||||
return 'name=' + JSON.stringify(name) + '; ext=' + JSON.stringify(ext);
|
||||
}
|
||||
|
||||
function showRestoreSelection(rows) {
|
||||
restoreListEl.textContent = '';
|
||||
const checkboxes = [];
|
||||
const statusKeys = {
|
||||
ready: 'restoreStatusReady',
|
||||
pluginApplied: 'restoreStatusReady',
|
||||
alreadyOriginal: 'restoreStatusAlreadyOriginal',
|
||||
changed: 'restoreStatusChanged',
|
||||
missing: 'restoreStatusMissing',
|
||||
error: 'restoreStatusError',
|
||||
};
|
||||
const updateApply = () => {
|
||||
const count = checkboxes.filter((input) => !input.disabled && input.checked).length;
|
||||
restoreApplyEl.disabled = count === 0;
|
||||
restoreApplyEl.textContent = t('thumb.restoreSelected', { count });
|
||||
};
|
||||
for (const row of rows) {
|
||||
const entry = row.entry;
|
||||
const canRestore = row.status === 'ready' || row.status === 'alreadyOriginal';
|
||||
const label = document.createElement('label');
|
||||
label.className = 'restore-row' + (canRestore ? '' : ' disabled');
|
||||
const input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.checked = canRestore;
|
||||
input.disabled = !canRestore;
|
||||
input.dataset.itemId = entry.itemId;
|
||||
input.addEventListener('change', updateApply);
|
||||
checkboxes.push(input);
|
||||
const details = document.createElement('div');
|
||||
const file = document.createElement('div');
|
||||
file.className = 'compat-file';
|
||||
file.textContent = entry.sourceName || entry.itemId;
|
||||
const location = document.createElement('div');
|
||||
location.className = 'compat-path';
|
||||
location.textContent = entry.folderPath || '';
|
||||
const current = document.createElement('div');
|
||||
current.className = 'restore-current';
|
||||
current.textContent = t('thumb.restoreCurrent') + ' ' + fieldsText(
|
||||
row.currentName === undefined ? '' : row.currentName,
|
||||
row.currentExt === undefined ? '' : row.currentExt,
|
||||
);
|
||||
const target = document.createElement('div');
|
||||
target.className = 'restore-target';
|
||||
target.textContent = t('thumb.restoreTarget') + ' ' + fieldsText(entry.originalName, entry.originalExt);
|
||||
const status = document.createElement('div');
|
||||
status.className = 'restore-status';
|
||||
if (row.status === 'ready' || row.status === 'pluginApplied') {
|
||||
status.classList.add('applied');
|
||||
status.textContent = t('thumb.restoreStatusReady', {
|
||||
fields: fieldsText(row.currentName, row.currentExt),
|
||||
});
|
||||
} else {
|
||||
status.textContent = t('thumb.' + statusKeys[row.status]);
|
||||
}
|
||||
details.append(file);
|
||||
if (location.textContent) details.append(location);
|
||||
details.append(current, target, status);
|
||||
label.append(input, details);
|
||||
restoreListEl.appendChild(label);
|
||||
}
|
||||
updateApply();
|
||||
restoreDialogEl.classList.remove('hidden');
|
||||
try {
|
||||
if (eagle.window && eagle.window.show) {
|
||||
Promise.resolve(eagle.window.show())
|
||||
.then(() => eagle.window.focus && eagle.window.focus())
|
||||
.catch(() => {});
|
||||
}
|
||||
} catch (error) { /* 列表仍可通过已打开的面板操作 */ }
|
||||
return new Promise((resolve) => {
|
||||
const finish = (value) => {
|
||||
restoreDialogEl.classList.add('hidden');
|
||||
restoreCancelEl.removeEventListener('click', cancel);
|
||||
restoreApplyEl.removeEventListener('click', apply);
|
||||
document.removeEventListener('keydown', keydown);
|
||||
resolve(value);
|
||||
};
|
||||
const cancel = () => finish(null);
|
||||
const apply = () => finish(checkboxes
|
||||
.filter((input) => !input.disabled && input.checked)
|
||||
.map((input) => input.dataset.itemId));
|
||||
const keydown = (event) => {
|
||||
if (event.key === 'Escape') cancel();
|
||||
};
|
||||
restoreCancelEl.addEventListener('click', cancel);
|
||||
restoreApplyEl.addEventListener('click', apply);
|
||||
document.addEventListener('keydown', keydown);
|
||||
const first = checkboxes.find((input) => !input.disabled);
|
||||
(first || restoreCancelEl).focus();
|
||||
});
|
||||
}
|
||||
|
||||
/** 修改前统一检查已有缩略图、真实文件名和用户确认,避免任何静默元数据变更。 */
|
||||
async function prepareCompoundCompatibility(items) {
|
||||
const changes = [];
|
||||
for (const item of items) {
|
||||
try {
|
||||
const eagleItem = await eagle.item.getById(itemIdOf(item.dirName));
|
||||
if (!eagleItem || eagleItem.customThumbnail === true) continue;
|
||||
const plan = compoundCompat.planChange(item, eagleItem, utils.splitCompoundName);
|
||||
if (!plan) continue;
|
||||
const label = item.entryName + ' | ' + folderPathOf(item.folders);
|
||||
if (plan.blocked) {
|
||||
item.compoundBlocked = plan;
|
||||
log(t('thumb.compoundSourceMismatch', {
|
||||
label,
|
||||
current: plan.currentFileName,
|
||||
source: plan.sourceName,
|
||||
}), 'warn');
|
||||
continue;
|
||||
}
|
||||
changes.push({ item, plan });
|
||||
} catch (error) {
|
||||
item.compoundBlocked = { error };
|
||||
log(t('thumb.compoundCheckFailed', {
|
||||
label: item.entryName,
|
||||
error: (error && error.message) || error,
|
||||
}), 'warn');
|
||||
}
|
||||
}
|
||||
if (!changes.length) return;
|
||||
if (!compatStore) {
|
||||
for (const change of changes) change.item.compoundBlocked = { error: 'backup unavailable' };
|
||||
log(t('thumb.compoundBackupUnavailable'), 'warn');
|
||||
return;
|
||||
}
|
||||
const selectedIds = new Set(await showCompoundConfirmation(changes));
|
||||
for (const change of changes) {
|
||||
if (selectedIds.has(itemIdOf(change.item.dirName))) change.item.compoundApproval = change.plan;
|
||||
else change.item.compoundDeclined = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function rollbackCompoundChange(eagleItem, backup, label) {
|
||||
if (!backup) return false;
|
||||
try {
|
||||
eagleItem.name = backup.originalName;
|
||||
eagleItem.ext = backup.originalExt;
|
||||
await withTimeout(eagleItem.save(), 20000, t('thumb.fixSuffixAction', { label }));
|
||||
compatStore.remove(backup.itemId);
|
||||
log(t('thumb.compoundRolledBack', { label }), 'warn');
|
||||
return true;
|
||||
} catch (error) {
|
||||
log(t('thumb.compoundRollbackFailed', {
|
||||
label,
|
||||
error: (error && error.message) || error,
|
||||
}), 'error');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 一个素材的缩略图生成流程:查重 → 确认兼容变更 → 渲染 → 写临时文件 → 设置 → 清理。 */
|
||||
async function generateOne(item) {
|
||||
let tempPath = null;
|
||||
let compatibilityAttempted = false;
|
||||
let compatibilityBackup = null;
|
||||
// 日志统一用「文件名 | Eagle 文件夹路径」,比 MU9RVSxxxx.info 好认;
|
||||
// 这些信息配对阶段就读到了,Eagle API 拿不到素材时也能照样显示。
|
||||
const label = item.entryName + ' | ' + folderPathOf(item.folders);
|
||||
@@ -176,21 +437,6 @@ async function generateOne(item) {
|
||||
log(t('thumb.missingItem', { label }), 'warn');
|
||||
return;
|
||||
}
|
||||
// 复合后缀(.skel.bytes / .atlas.txt):Eagle 只按最后一个点拆 name/ext,atlas.txt
|
||||
// 会被当成纯文本,卡片写死成文本预览,自定义缩略图根本显示不出来。把整段后缀塞进
|
||||
// ext 后 Eagle 认不出这个类型,就会用我们生成的缩略图当封面。
|
||||
// 只在「name + '.' + ext 就是磁盘上的真实文件名」时才改,避免覆盖用户自己改过的名字。
|
||||
const parts = utils.splitCompoundName(eagleItem.name + (eagleItem.ext ? '.' + eagleItem.ext : ''));
|
||||
if (parts && (eagleItem.name !== parts.name || eagleItem.ext !== parts.ext)) {
|
||||
try {
|
||||
eagleItem.name = parts.name;
|
||||
eagleItem.ext = parts.ext;
|
||||
await withTimeout(eagleItem.save(), 20000, t('thumb.fixSuffixAction', { label }));
|
||||
log(t('thumb.suffixFixed', { label, name: parts.name + '.' + parts.ext }), 'dim');
|
||||
} catch (error) {
|
||||
log(t('thumb.suffixFailed', { label, error: (error && error.message) || error }), 'warn');
|
||||
}
|
||||
}
|
||||
// 不覆盖用户已有的自定义缩略图(Eagle 审核要求避免静默覆盖用户数据)。
|
||||
if (eagleItem.customThumbnail === true) {
|
||||
stats.skipped += 1;
|
||||
@@ -198,6 +444,47 @@ async function generateOne(item) {
|
||||
log(t('thumb.existingThumbnail', { label }), 'dim');
|
||||
return;
|
||||
}
|
||||
if (item.compoundDeclined || item.compoundBlocked) {
|
||||
stats.skipped += 1;
|
||||
countRun('skipped', false, item.entryName);
|
||||
log(t('thumb.compoundSkipped', { label }), 'warn');
|
||||
return;
|
||||
}
|
||||
const plan = compoundCompat.planChange(item, eagleItem, utils.splitCompoundName);
|
||||
if (plan && plan.blocked) {
|
||||
stats.skipped += 1;
|
||||
countRun('skipped', false, item.entryName);
|
||||
log(t('thumb.compoundSourceMismatch', {
|
||||
label,
|
||||
current: plan.currentFileName,
|
||||
source: plan.sourceName,
|
||||
}), 'warn');
|
||||
return;
|
||||
}
|
||||
if (plan) {
|
||||
const approved = item.compoundApproval;
|
||||
if (!approved || !compoundMatchesApproval(plan, approved)) {
|
||||
stats.skipped += 1;
|
||||
countRun('skipped', false, item.entryName);
|
||||
log(t('thumb.compoundNeedsConfirmation', { label }), 'warn');
|
||||
return;
|
||||
}
|
||||
if (!compatStore) throw new Error(t('thumb.compoundBackupUnavailable'));
|
||||
compatibilityBackup = compatStore.remember({
|
||||
itemId,
|
||||
sourceName: plan.sourceName,
|
||||
folderPath: folderPathOf(item.folders),
|
||||
currentName: plan.currentName,
|
||||
currentExt: plan.currentExt,
|
||||
newName: plan.newName,
|
||||
newExt: plan.newExt,
|
||||
});
|
||||
eagleItem.name = plan.newName;
|
||||
eagleItem.ext = plan.newExt;
|
||||
compatibilityAttempted = true;
|
||||
await withTimeout(eagleItem.save(), 20000, t('thumb.fixSuffixAction', { label }));
|
||||
log(t('thumb.suffixFixed', { label, name: plan.newFileName }), 'dim');
|
||||
}
|
||||
const version = utils.detectVersion(item.skeletonPath);
|
||||
const runtimeDir = utils.chooseRuntimeDir(version, item.isBinary);
|
||||
// 动态缩略图要逐帧渲染再逐档试体积,比一张静帧慢得多,超时时间分开给。
|
||||
@@ -228,6 +515,9 @@ async function generateOne(item) {
|
||||
countRun('generated', rendered.mode === 'animated', item.entryName);
|
||||
log(t('thumb.generated', { label, details: describeThumbnail(rendered), version }), 'ok');
|
||||
} catch (error) {
|
||||
if (compatibilityAttempted) {
|
||||
await rollbackCompoundChange(eagleItem, compatibilityBackup, label);
|
||||
}
|
||||
stats.failed += 1;
|
||||
countRun('failed', false, item.entryName);
|
||||
log(t('thumb.generateFailed', { label, error: (error && error.message) || error }), 'fail');
|
||||
@@ -400,6 +690,7 @@ async function handleBatchAsync({ added, total, reason }) {
|
||||
setStatus(t('thumb.statusAutoSkipped'));
|
||||
return;
|
||||
}
|
||||
await prepareCompoundCompatibility(ready);
|
||||
queue.push(...ready);
|
||||
// 已经在跑的一轮又来新素材,把总数补上,进度条才不会一直显示满。
|
||||
if (runStats) runStats.total += ready.length;
|
||||
@@ -450,12 +741,57 @@ async function start() {
|
||||
}
|
||||
|
||||
function startSafely() {
|
||||
if (instanceCoordinator && !instanceCoordinator.isActive()) return;
|
||||
start().catch((error) => {
|
||||
setStatus(t('thumb.statusStartFailed', { error: (error && error.message) || error }));
|
||||
log(t('thumb.statusStartFailed', { error: (error && error.stack) || error }), 'error');
|
||||
});
|
||||
}
|
||||
|
||||
async function initializeInstanceCoordinator(plugin) {
|
||||
if (instanceCoordinator) {
|
||||
instanceCoordinator.evaluate();
|
||||
return;
|
||||
}
|
||||
await initializeCompatStore();
|
||||
const stateFile = pluginUserDataPath
|
||||
? path.join(pluginUserDataPath, 'SpineThumbnailGenerateServer', 'active-instance.json')
|
||||
: path.join(os.tmpdir(), 'spine-thumbnail-active-instance.json');
|
||||
const storage = instanceCoordinatorModule.createFileStorage(stateFile);
|
||||
const id = String(window.windowID || (plugin && plugin.webContentsID) || Date.now());
|
||||
instanceCoordinator = instanceCoordinatorModule.createCoordinator({
|
||||
storage,
|
||||
id,
|
||||
isVisible: () => document.visibilityState === 'visible',
|
||||
hide: () => eagle.window && eagle.window.hide && eagle.window.hide(),
|
||||
onActiveChanged(active) {
|
||||
if (active) {
|
||||
startSafely();
|
||||
return;
|
||||
}
|
||||
libraryWatcher.stop();
|
||||
queue.length = 0;
|
||||
try { if (eagle.window && eagle.window.hide) eagle.window.hide(); } catch (error) { /* already hidden */ }
|
||||
},
|
||||
intervalMs: 250,
|
||||
});
|
||||
document.addEventListener('visibilitychange', () => instanceCoordinator.handleVisibility());
|
||||
window.addEventListener('beforeunload', () => instanceCoordinator.stop());
|
||||
instanceCoordinator.start();
|
||||
}
|
||||
|
||||
async function initializeCompatStore() {
|
||||
if (compatStore) return true;
|
||||
try {
|
||||
pluginUserDataPath = await eagle.app.getPath('userData');
|
||||
compatStore = compoundCompat.createStore(pluginUserDataPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log(t('thumb.compoundBackupUnavailable') + ': ' + ((error && error.message) || error), 'warn');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
try {
|
||||
settings = await settingsStore.read();
|
||||
@@ -490,6 +826,44 @@ clearEl.addEventListener('click', () => {
|
||||
logEl.textContent = '';
|
||||
});
|
||||
|
||||
restoreNamesEl.addEventListener('click', async () => {
|
||||
if (!compatStore) {
|
||||
log(t('thumb.compoundBackupUnavailable'), 'warn');
|
||||
return;
|
||||
}
|
||||
let rows;
|
||||
try {
|
||||
rows = await compoundCompat.inspectBackups(
|
||||
compatStore,
|
||||
(itemId) => eagle.item.getById(itemId),
|
||||
backupSourceExists,
|
||||
);
|
||||
} catch (error) {
|
||||
log(t('thumb.restoreFailed', { error: (error && error.message) || error }), 'error');
|
||||
return;
|
||||
}
|
||||
if (!rows.length) {
|
||||
log(t('thumb.restoreEmpty'), 'dim');
|
||||
return;
|
||||
}
|
||||
const selected = await showRestoreSelection(rows);
|
||||
if (!selected || !selected.length) return;
|
||||
restoreNamesEl.disabled = true;
|
||||
try {
|
||||
const result = await compoundCompat.restoreBackups(
|
||||
compatStore,
|
||||
selected,
|
||||
(itemId) => eagle.item.getById(itemId),
|
||||
(item) => withTimeout(item.save(), 20000, t('thumb.restoreNames')),
|
||||
);
|
||||
log(t('thumb.restoreComplete', result), result.failed ? 'warn' : 'ok');
|
||||
} catch (error) {
|
||||
log(t('thumb.restoreFailed', { error: (error && error.message) || error }), 'error');
|
||||
} finally {
|
||||
restoreNamesEl.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
modeEl.addEventListener('change', async () => {
|
||||
settings = { ...settings, mode: modeEl.value === 'static' ? 'static' : 'animated' };
|
||||
try {
|
||||
@@ -535,12 +909,13 @@ if (typeof eagle === 'undefined') {
|
||||
log(t('thumb.pluginCreated', {
|
||||
name: (plugin && plugin.manifest && plugin.manifest.name) || t('thumb.title'),
|
||||
}));
|
||||
await initializeCompatStore();
|
||||
try {
|
||||
await loadSettings();
|
||||
} catch (error) {
|
||||
log(t('thumb.settingsReadFailed', { error: (error && error.message) || error }), 'warn');
|
||||
}
|
||||
startSafely();
|
||||
initializeInstanceCoordinator(plugin);
|
||||
});
|
||||
eagle.onLibraryChanged((changedPath) => {
|
||||
log(t('thumb.switchingLibrary', { path: changedPath }));
|
||||
@@ -552,7 +927,8 @@ if (typeof eagle === 'undefined') {
|
||||
if (i18n) i18n.setLocale(i18n.requestedLocale());
|
||||
logVersion();
|
||||
log(t('thumb.noCreateEvent'), 'dim');
|
||||
loadSettings().catch(() => {}).then(startSafely);
|
||||
Promise.all([initializeCompatStore(), loadSettings().catch(() => {})])
|
||||
.then(() => initializeInstanceCoordinator());
|
||||
}
|
||||
}, 800);
|
||||
}
|
||||
@@ -203,7 +203,7 @@ function splitCompoundName(fileName) {
|
||||
if (!suffix) return null;
|
||||
const name = String(fileName).slice(0, fileName.length - suffix.length);
|
||||
if (!name) return null;
|
||||
return { name, ext: suffix.slice(1) };
|
||||
return { name, ext: String(fileName).slice(-suffix.length + 1) };
|
||||
}
|
||||
|
||||
/** 从 JSON 文本中读取 spine 版本 */
|
||||
|
||||
Reference in new issue
Block a user