八国语言+eagle图片

This commit is contained in:
tianmo committed 2026-10-29 11:02:01 +08:00
1 parent acc93d5404
commit 3bd3ba7404
46 files changed
+2520 -290

No files matched your search

@@ -0,0 +1,117 @@
{
"manifest": {
"app": {
"name": "Spine-Vorschaubilder"
}
},
"thumb": {
"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.",
"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",
"modeLabel": "Erstellungsmodus",
"modeAnimated": "Animiert (erste Animation)",
"modeStatic": "Statisch (Setup-Pose)",
"strategyLabel": "Strategie",
"strategySize": "Kleinste Datei (langsam)",
"strategySpeed": "Schnellste Methode (schnell)",
"statusLabel": "Status",
"statusStarting": "Wird gestartet…",
"libraryLabel": "Bibliothek",
"statsLabel": "Statistik",
"filterAll": "Alle",
"filterOk": "Erfolg",
"filterFail": "Fehlgeschlagen",
"filterWarn": "Warnung",
"filterError": "Fehler",
"unknownError": "Unbekannter Fehler",
"scriptError": "Skriptfehler: {error}",
"promiseError": "Unbehandelter Promise-Fehler: {error}",
"uncategorized": "Ohne Kategorie",
"reasonQuiet": "Ruhephase",
"reasonFallback": "Ersatzprüfung",
"none": "keine",
"complete": "vollständig",
"incomplete": "unvollständig",
"statusLoaded": "Skript {version} geladen; warte auf Eagle-Ereignisse…",
"statusEagleMissing": "Eagle-API nicht verfügbar",
"statusListening": "Überwachung aktiv",
"statusListenFailed": "Überwachung fehlgeschlagen",
"statusAutoOff": "Überwachung aktiv (automatische Erstellung aus)",
"statusNoLibrary": "Bibliothekspfad nicht verfügbar",
"statusPairing": "Neue Assets werden zugeordnet…",
"statusPairingProgress": "Zuordnung {current}/{total}",
"statusRendering": "Frame wird gerendert {current}/{total} ({queued} in Warteschlange)",
"statusGenerating": "Vorschaubilder werden erstellt… noch {queued}",
"statusPaused": "Pausiert (im Fortschrittsfenster fortsetzen oder abbrechen)",
"statusAutoSkipped": "Automatische Erstellung ist aus; dieser Stapel wurde übersprungen",
"statusStartFailed": "Start fehlgeschlagen: {error}",
"stats": "Stapel {batches} · Hinzugefügt {added} · Spine {spine} · Vollständig {complete} · Erstellt {generated} · Übersprungen {skipped} · Fehlgeschlagen {failed}",
"modulesMissing": "Lokale Module nicht geladen: {modules}",
"moduleLoadError": "Lokale Module wurden nicht geladen. Prüfe die Skriptreihenfolge in index.html: {modules}",
"version": "Plugin-Codeversion {version}",
"pluginCreated": "Plugin erstellt: {name}",
"settingsReadFailed": "Einstellungen konnten nicht gelesen werden; Standardwerte werden verwendet (automatische Erstellung aus): {error}",
"settingsSaveFailed": "Einstellungen konnten nicht gespeichert werden: {error}",
"autoEnabled": "Automatische Erstellung aktiviert. Neu importierte Spine-Assets erhalten Vorschaubilder.",
"autoDisabled": "Automatische Erstellung deaktiviert; die Warteschlange wurde geleert.",
"modeStaticLog": "Erstellungsmodus: statisch (immer Setup-Pose, PNG)",
"modeAnimatedLog": "Erstellungsmodus: animiert (erste Animation als WebP; Setup-Pose ohne Animation)",
"strategySpeedLog": "Strategie: schnellste Methode (grobe Auswahl, nur die kleinsten ein oder zwei Profile)",
"strategySizeLog": "Strategie: kleinste Datei (alle zehn Profile erstellen und das kleinste Ergebnis behalten)",
"missingItem": "{label} übersprungen: Element nicht in Eagle gefunden",
"suffixFixed": "Zusammengesetzte Endung für {label} korrigiert → {name}",
"suffixFailed": "Zusammengesetzte Endung für {label} konnte nicht korrigiert werden: {error}",
"existingThumbnail": "{label} übersprungen: eigenes Vorschaubild vorhanden",
"thumbnailRejected": "Eagle hat dieses Vorschaubild abgelehnt",
"generated": "{label} erstellt | {details} | Spine {version}",
"generateFailed": "Erstellung für {label} fehlgeschlagen: {error}",
"cancelled": "Erstellung abgebrochen: {dropped} ausstehende Elemente verworfen. {done} abgeschlossen (Erfolg {generated}, Fehler {failed}, übersprungen {skipped})",
"batchFailed": "Importstapel konnte nicht verarbeitet werden: {error}",
"batchFinished": "Stapel abgeschlossen: {added} hinzugefügt, {total} in der Bibliothek ({reason})",
"recheck": "{count} früher unvollständige Assets werden erneut geprüft",
"noSpine": "Keine Spine-Assets in diesem Stapel",
"incompletePending": "{count} Assets sind unvollständig und werden nach späteren Importen erneut geprüft",
"incompleteIgnored": "{count} Assets sind unvollständig und wurden in diesem Stapel nicht verarbeitet",
"itemSummary": " {name} | {path} | {state} | Skelett {skeleton} | Atlas {atlas} | Seiten {pages} (fehlend {missing})",
"autoSkipped": "Automatische Erstellung ist aus; {count} Assets in diesem Stapel übersprungen",
"queued": "{added} zur Warteschlange hinzugefügt; {queued} warten{state}",
"queuedRunning": " (wird verarbeitet)",
"libraryInfoNoPath": "library.info() hat keinen Pfad zurückgegeben: {info}",
"libraryInfoFailed": "library.info() fehlgeschlagen: {error}",
"switchingLibrary": "Bibliothek wird gewechselt: {path}",
"noCreateEvent": "Kein Plugin-Erstellungsereignis erhalten; direkter Start wird versucht",
"watching": "{path} wird überwacht ({count} vorhandene Elemente gelten als Verlauf)",
"watchReadFailed": "Asset-Verzeichnis konnte nicht gelesen werden: {error}",
"watchMissingDir": "Asset-Verzeichnis fehlt: {path}",
"watchFailed": "Überwachung konnte nicht gestartet werden: {error}",
"fixSuffixAction": "Zusammengesetzte Endung für {label} korrigieren",
"renderAction": "{label} rendern",
"setThumbnailAction": "Vorschaubild für {label} setzen",
"thumbnailDetailsAnimated": "Animiert {size}@{fps}fps/q{quality}/{kb}KB (Ziel {target}KB)/{frames} Frames/Animation {animation}/{strategy}{limit}",
"thumbnailDetailsStatic": "Statisch {width}×{height}/{kb}KB{fallback}",
"bestSpeed": "schnellste Methode",
"bestSize": "kleinste Datei",
"fallbackNote": " ({text})",
"fallbackNoAnimation": "das Skelett hat keine Animationen",
"fallbackAnimatedFailed": "animierte Erstellung fehlgeschlagen: {error}",
"limitNote": " (auch das kleinste Profil überschreitet das Ziel; erhöhe gegebenenfalls die Vorschaubildgröße)",
"runSummary": "{generated} erfolgreich, {failed} fehlgeschlagen, {skipped} übersprungen in {seconds} Sekunden."
},
"progress": {
"title": "Spine-Vorschaubilder",
"generating": "Animierte Vorschaubilder werden erstellt…",
"waiting": "Warte auf Start…",
"confirmPrompt": "Diesen Lauf abbrechen?",
"resume": "Fortsetzen",
"cancel": "Abbruch bestätigen",
"stop": "Stoppen",
"confirm": "Fertig",
"generatedConfirm": "{done} Vorschaubilder erstellt. Lauf abbrechen?",
"complete": "Abgeschlossen",
"completed": "Abgeschlossen {done} / {total} ({percent}%)"
}
}
@@ -0,0 +1,117 @@
{
"manifest": {
"app": {
"name": "Spine Thumbnails"
}
},
"thumb": {
"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.",
"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",
"modeLabel": "Generation mode",
"modeAnimated": "Animated (first animation)",
"modeStatic": "Still (setup pose)",
"strategyLabel": "Strategy",
"strategySize": "Smallest file (slow)",
"strategySpeed": "Fastest (quick)",
"statusLabel": "Status",
"statusStarting": "Starting…",
"libraryLabel": "Library",
"statsLabel": "Statistics",
"filterAll": "All",
"filterOk": "Success",
"filterFail": "Failed",
"filterWarn": "Warning",
"filterError": "Error",
"unknownError": "Unknown error",
"scriptError": "Script error: {error}",
"promiseError": "Unhandled Promise error: {error}",
"uncategorized": "Uncategorized",
"reasonQuiet": "quiet",
"reasonFallback": "fallback scan",
"none": "none",
"complete": "complete",
"incomplete": "incomplete",
"statusLoaded": "Script {version} loaded; waiting for Eagle events…",
"statusEagleMissing": "Eagle API not available",
"statusListening": "Watching",
"statusListenFailed": "Watch failed",
"statusAutoOff": "Watching (automatic generation off)",
"statusNoLibrary": "Library path unavailable",
"statusPairing": "Pairing new assets…",
"statusPairingProgress": "Pairing {current}/{total}",
"statusRendering": "Rendering frame {current}/{total} ({queued} queued)",
"statusGenerating": "Generating thumbnails… {queued} remaining",
"statusPaused": "Paused (resume or cancel in the progress window)",
"statusAutoSkipped": "Automatic generation is off; this batch was skipped",
"statusStartFailed": "Startup failed: {error}",
"stats": "Batches {batches} · Added {added} · Spine {spine} · Complete {complete} · Generated {generated} · Skipped {skipped} · Failed {failed}",
"modulesMissing": "Local modules not loaded: {modules}",
"moduleLoadError": "Local modules were not loaded. Check the script order in index.html: {modules}",
"version": "Plugin code version {version}",
"pluginCreated": "Plugin created: {name}",
"settingsReadFailed": "Failed to read settings; using defaults (automatic generation off): {error}",
"settingsSaveFailed": "Failed to save settings: {error}",
"autoEnabled": "Automatic generation enabled. Newly imported Spine assets will receive thumbnails.",
"autoDisabled": "Automatic generation disabled; the pending queue was cleared.",
"modeStaticLog": "Generation mode: still (setup pose, PNG)",
"modeAnimatedLog": "Generation mode: animated (first animation as WebP; setup pose if no animation exists)",
"strategySpeedLog": "Strategy: fastest (estimate profiles and render only the smallest one or two)",
"strategySizeLog": "Strategy: smallest file (render all ten profiles and keep the smallest result)",
"missingItem": "Skipped {label}: item not found in Eagle",
"suffixFixed": "Fixed compound suffix for {label} → {name}",
"suffixFailed": "Failed to fix compound suffix for {label}: {error}",
"existingThumbnail": "Skipped {label}: a custom thumbnail already exists",
"thumbnailRejected": "Eagle rejected this thumbnail",
"generated": "Generated {label} | {details} | Spine {version}",
"generateFailed": "Generation failed for {label}: {error}",
"cancelled": "Generation cancelled: discarded {dropped} pending items. Completed {done} (success {generated}, failed {failed}, skipped {skipped})",
"batchFailed": "Failed to process import batch: {error}",
"batchFinished": "Batch finished: {added} added, {total} in library ({reason})",
"recheck": "Rechecking {count} previously incomplete assets",
"noSpine": "No Spine assets in this batch",
"incompletePending": "{count} assets are incomplete and will be rechecked after later imports",
"incompleteIgnored": "{count} assets are incomplete and were not processed",
"itemSummary": " {name} | {path} | {state} | skeleton {skeleton} | atlas {atlas} | pages {pages} (missing {missing})",
"autoSkipped": "Automatic generation is off; skipped {count} assets in this batch",
"queued": "Queued {added}; {queued} waiting{state}",
"queuedRunning": " (processing)",
"libraryInfoNoPath": "library.info() did not return a path: {info}",
"libraryInfoFailed": "library.info() failed: {error}",
"switchingLibrary": "Switching library: {path}",
"noCreateEvent": "No plugin-create event received; trying to start directly",
"watching": "Watching {path} ({count} existing items treated as history)",
"watchReadFailed": "Failed to read the asset directory: {error}",
"watchMissingDir": "Asset directory does not exist: {path}",
"watchFailed": "Failed to start watching: {error}",
"fixSuffixAction": "Fixing compound suffix for {label}",
"renderAction": "Rendering {label}",
"setThumbnailAction": "Setting thumbnail for {label}",
"thumbnailDetailsAnimated": "Animated {size}@{fps}fps/q{quality}/{kb}KB (target {target}KB)/{frames} frames/animation {animation}/{strategy}{limit}",
"thumbnailDetailsStatic": "Still {width}×{height}/{kb}KB{fallback}",
"bestSpeed": "fastest",
"bestSize": "smallest file",
"fallbackNote": " ({text})",
"fallbackNoAnimation": "the skeleton has no animations",
"fallbackAnimatedFailed": "animated generation failed: {error}",
"limitNote": " (the smallest profile still exceeds the target; consider a larger thumbnail size)",
"runSummary": "{generated} succeeded, {failed} failed, {skipped} skipped in {seconds} seconds."
},
"progress": {
"title": "Spine Thumbnails",
"generating": "Generating animated thumbnails…",
"waiting": "Waiting to start…",
"confirmPrompt": "Cancel this run?",
"resume": "Continue",
"cancel": "Confirm cancel",
"stop": "Stop",
"confirm": "Done",
"generatedConfirm": "{done} thumbnails generated. Cancel this run?",
"complete": "Complete",
"completed": "Completed {done} / {total} ({percent}%)"
}
}
@@ -0,0 +1,117 @@
{
"manifest": {
"app": {
"name": "Miniaturas de Spine"
}
},
"thumb": {
"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.",
"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",
"modeLabel": "Modo de generación",
"modeAnimated": "Animado (primera animación)",
"modeStatic": "Estático (pose de configuración)",
"strategyLabel": "Estrategia",
"strategySize": "Archivo más pequeño (lento)",
"strategySpeed": "Más rápido (rápido)",
"statusLabel": "Estado",
"statusStarting": "Iniciando…",
"libraryLabel": "Biblioteca",
"statsLabel": "Estadísticas",
"filterAll": "Todo",
"filterOk": "Correcto",
"filterFail": "Fallido",
"filterWarn": "Aviso",
"filterError": "Error",
"unknownError": "Error desconocido",
"scriptError": "Error de script: {error}",
"promiseError": "Error de Promise no controlado: {error}",
"uncategorized": "Sin categoría",
"reasonQuiet": "fin de actividad",
"reasonFallback": "exploración de respaldo",
"none": "ninguno",
"complete": "completo",
"incomplete": "incompleto",
"statusLoaded": "Script {version} cargado; esperando eventos de Eagle…",
"statusEagleMissing": "API de Eagle no disponible",
"statusListening": "Supervisando",
"statusListenFailed": "Error de supervisión",
"statusAutoOff": "Supervisando (generación automática desactivada)",
"statusNoLibrary": "Ruta de la biblioteca no disponible",
"statusPairing": "Emparejando recursos nuevos…",
"statusPairingProgress": "Emparejando {current}/{total}",
"statusRendering": "Renderizando fotograma {current}/{total} ({queued} en cola)",
"statusGenerating": "Generando miniaturas… quedan {queued}",
"statusPaused": "En pausa (continúa o cancela en la ventana de progreso)",
"statusAutoSkipped": "La generación automática está desactivada; se omitió este lote",
"statusStartFailed": "Error al iniciar: {error}",
"stats": "Lotes {batches} · Añadidos {added} · Spine {spine} · Completos {complete} · Generados {generated} · Omitidos {skipped} · Fallidos {failed}",
"modulesMissing": "Módulos locales no cargados: {modules}",
"moduleLoadError": "No se cargaron los módulos locales. Revisa el orden de scripts en index.html: {modules}",
"version": "Versión del código del complemento {version}",
"pluginCreated": "Complemento creado: {name}",
"settingsReadFailed": "No se pudieron leer los ajustes; se usarán los valores predeterminados (generación automática desactivada): {error}",
"settingsSaveFailed": "No se pudieron guardar los ajustes: {error}",
"autoEnabled": "Generación automática activada. Los nuevos recursos de Spine importados recibirán miniaturas.",
"autoDisabled": "Generación automática desactivada; se vació la cola pendiente.",
"modeStaticLog": "Modo de generación: estático (siempre pose de configuración, PNG)",
"modeAnimatedLog": "Modo de generación: animado (primera animación como WebP; pose de configuración si no hay animación)",
"strategySpeedLog": "Estrategia: más rápida (estimación y solo los perfiles más pequeños)",
"strategySizeLog": "Estrategia: archivo más pequeño (genera los diez perfiles y conserva el menor)",
"missingItem": "Se omitió {label}: elemento no encontrado en Eagle",
"suffixFixed": "Extensión compuesta corregida para {label} → {name}",
"suffixFailed": "No se pudo corregir la extensión compuesta de {label}: {error}",
"existingThumbnail": "Se omitió {label}: ya existe una miniatura personalizada",
"thumbnailRejected": "Eagle rechazó esta miniatura",
"generated": "{label} generado | {details} | Spine {version}",
"generateFailed": "Error al generar {label}: {error}",
"cancelled": "Generación cancelada: se descartaron {dropped} pendientes. Completados {done} (correctos {generated}, fallidos {failed}, omitidos {skipped})",
"batchFailed": "Error al procesar el lote importado: {error}",
"batchFinished": "Lote finalizado: {added} añadidos, {total} en la biblioteca ({reason})",
"recheck": "Revisando de nuevo {count} recursos previamente incompletos",
"noSpine": "No hay recursos de Spine en este lote",
"incompletePending": "{count} recursos están incompletos y se revisarán tras importaciones posteriores",
"incompleteIgnored": "{count} recursos están incompletos y no se procesaron en este lote",
"itemSummary": " {name} | {path} | {state} | esqueleto {skeleton} | atlas {atlas} | páginas {pages} (faltan {missing})",
"autoSkipped": "La generación automática está desactivada; se omitieron {count} recursos de este lote",
"queued": "{added} añadidos a la cola; {queued} esperando{state}",
"queuedRunning": " (procesando)",
"libraryInfoNoPath": "library.info() no devolvió una ruta: {info}",
"libraryInfoFailed": "Error de library.info(): {error}",
"switchingLibrary": "Cambiando de biblioteca: {path}",
"noCreateEvent": "No se recibió el evento de creación; intentando iniciar directamente",
"watching": "Supervisando {path} ({count} elementos existentes tratados como historial)",
"watchReadFailed": "No se pudo leer el directorio de recursos: {error}",
"watchMissingDir": "No existe el directorio de recursos: {path}",
"watchFailed": "No se pudo iniciar la supervisión: {error}",
"fixSuffixAction": "Corrigiendo extensión compuesta de {label}",
"renderAction": "Renderizando {label}",
"setThumbnailAction": "Estableciendo miniatura de {label}",
"thumbnailDetailsAnimated": "Animado {size}@{fps}fps/q{quality}/{kb}KB (objetivo {target}KB)/{frames} fotogramas/animación {animation}/{strategy}{limit}",
"thumbnailDetailsStatic": "Estático {width}×{height}/{kb}KB{fallback}",
"bestSpeed": "más rápido",
"bestSize": "archivo más pequeño",
"fallbackNote": " ({text})",
"fallbackNoAnimation": "el esqueleto no tiene animaciones",
"fallbackAnimatedFailed": "falló la generación animada: {error}",
"limitNote": " (incluso el perfil más pequeño supera el objetivo; considera aumentar el tamaño de la miniatura)",
"runSummary": "{generated} correctos, {failed} fallidos, {skipped} omitidos en {seconds} segundos."
},
"progress": {
"title": "Miniaturas de Spine",
"generating": "Generando miniaturas animadas…",
"waiting": "Esperando para iniciar…",
"confirmPrompt": "¿Cancelar esta ejecución?",
"resume": "Continuar",
"cancel": "Confirmar cancelación",
"stop": "Detener",
"confirm": "Listo",
"generatedConfirm": "Se generaron {done} miniaturas. ¿Cancelar esta ejecución?",
"complete": "Completado",
"completed": "Completado {done} / {total} ({percent}%)"
}
}
@@ -0,0 +1,117 @@
{
"manifest": {
"app": {
"name": "Spineサムネイル"
}
},
"thumb": {
"title": "Spineサムネイル",
"noticeIntro": "Spine アセット(.skel / .json + .atlas + テクスチャ)を読み込んだ後、カスタムサムネイルがないアイテムにスケルトンプレビューを生成します。",
"noticeSafety": "今回新しく読み込んだアイテムだけを処理し、既存アイテムの走査や変更、カスタムサムネイルの上書きは行いません。",
"noticeModes": "アニメーションは最初のアニメーションを WebP に変換します。静止画は常に setup pose を使用します。",
"noticeDefault": "自動生成は既定でオフです。必要なときに下で有効にしてください。",
"autoToggle": "読み込み時にサムネイルを自動生成",
"clear": "ログを消去",
"modeLabel": "生成方式",
"modeAnimated": "アニメーション(最初のアニメーション)",
"modeStatic": "静止画(setup pose)",
"strategyLabel": "生成戦略",
"strategySize": "最小サイズ(低速)",
"strategySpeed": "最速(高速)",
"statusLabel": "状態",
"statusStarting": "起動中…",
"libraryLabel": "ライブラリ",
"statsLabel": "統計",
"filterAll": "すべて",
"filterOk": "成功",
"filterFail": "失敗",
"filterWarn": "警告",
"filterError": "エラー",
"unknownError": "不明なエラー",
"scriptError": "スクリプトエラー:{error}",
"promiseError": "未処理の Promise エラー:{error}",
"uncategorized": "未分類",
"reasonQuiet": "静止検出",
"reasonFallback": "フォールバック走査",
"none": "なし",
"complete": "完全",
"incomplete": "不完全",
"statusLoaded": "スクリプト {version} を読み込みました。Eagle イベントを待機しています…",
"statusEagleMissing": "Eagle API が利用できません",
"statusListening": "監視中",
"statusListenFailed": "監視に失敗",
"statusAutoOff": "監視中(自動生成はオフ)",
"statusNoLibrary": "ライブラリのパスを取得できません",
"statusPairing": "新しいアセットを照合中…",
"statusPairingProgress": "照合中 {current}/{total}",
"statusRendering": "フレームをレンダリング中 {current}/{total}(待機 {queued})",
"statusGenerating": "サムネイルを生成中…残り {queued}",
"statusPaused": "一時停止中(進捗ウィンドウで再開またはキャンセル)",
"statusAutoSkipped": "自動生成がオフのため、このバッチをスキップしました",
"statusStartFailed": "起動に失敗:{error}",
"stats": "バッチ {batches} · 追加 {added} · Spine {spine} · 完全 {complete} · 生成済み {generated} · スキップ {skipped} · 失敗 {failed}",
"modulesMissing": "ローカルモジュールが未読み込みです:{modules}",
"moduleLoadError": "ローカルモジュールが読み込まれていません。index.html の script 順を確認してください:{modules}",
"version": "プラグインコード {version}",
"pluginCreated": "プラグインを初期化しました:{name}",
"settingsReadFailed": "設定を読み込めないため既定値を使用します(自動生成オフ):{error}",
"settingsSaveFailed": "設定を保存できません:{error}",
"autoEnabled": "自動生成を有効にしました。今後読み込む Spine アセットにサムネイルを生成します。",
"autoDisabled": "自動生成を無効にし、待機中のキューを消去しました。",
"modeStaticLog": "生成方式:静止画(常に setup pose、PNG)",
"modeAnimatedLog": "生成方式:アニメーション(最初のアニメーションを WebP 化し、なければ setup pose)",
"strategySpeedLog": "生成戦略:最速(概算で選び、最小の 1~2 プロファイルのみ生成)",
"strategySizeLog": "生成戦略:最小サイズ(全 10 プロファイルを生成し最小を採用)",
"missingItem": "{label} をスキップ:Eagle にアイテムが見つかりません",
"suffixFixed": "{label} の複合拡張子を修正 → {name}",
"suffixFailed": "{label} の複合拡張子を修正できません:{error}",
"existingThumbnail": "{label} をスキップ:カスタムサムネイルが既にあります",
"thumbnailRejected": "Eagle がこのサムネイルを拒否しました",
"generated": "{label} を生成 | {details} | Spine {version}",
"generateFailed": "{label} の生成に失敗:{error}",
"cancelled": "生成をキャンセル:残り {dropped} 件を破棄、完了 {done} 件(成功 {generated}、失敗 {failed}、スキップ {skipped})",
"batchFailed": "読み込みバッチの処理に失敗:{error}",
"batchFinished": "バッチ完了:{added} 件追加、ライブラリ内 {total} 件({reason})",
"recheck": "以前の不完全なアセット {count} 件を再確認",
"noSpine": "このバッチに Spine アセットはありません",
"incompletePending": "不完全なアセットが {count} 件あります。後の読み込み後に再確認します",
"incompleteIgnored": "不完全なアセットが {count} 件あり、このバッチでは処理しません",
"itemSummary": " {name} | {path} | {state} | スケルトン {skeleton} | atlas {atlas} | ページ {pages}(不足 {missing})",
"autoSkipped": "自動生成がオフのため、このバッチの {count} 件をスキップしました",
"queued": "{added} 件をキューに追加、待機中 {queued} 件{state}",
"queuedRunning": "(処理中)",
"libraryInfoNoPath": "library.info() がパスを返しません:{info}",
"libraryInfoFailed": "library.info() の呼び出しに失敗:{error}",
"switchingLibrary": "ライブラリを切り替え:{path}",
"noCreateEvent": "作成イベントを受信できないため、直接起動します",
"watching": "{path} を監視中(既存 {count} 件は履歴として扱い、処理しません)",
"watchReadFailed": "アセットディレクトリを読み込めません:{error}",
"watchMissingDir": "アセットディレクトリがありません:{path}",
"watchFailed": "監視を開始できません:{error}",
"fixSuffixAction": "{label} の複合拡張子を修正",
"renderAction": "{label} をレンダリング",
"setThumbnailAction": "{label} のサムネイルを設定",
"thumbnailDetailsAnimated": "アニメーション {size}@{fps}fps/q{quality}/{kb}KB(目標 {target}KB)/ {frames} フレーム / アニメーション {animation}/{strategy}{limit}",
"thumbnailDetailsStatic": "静止画 {width}×{height}/{kb}KB{fallback}",
"bestSpeed": "最速",
"bestSize": "最小サイズ",
"fallbackNote": "({text})",
"fallbackNoAnimation": "スケルトンにアニメーションがありません",
"fallbackAnimatedFailed": "アニメーション生成に失敗:{error}",
"limitNote": "(最小プロファイルでも目標サイズを超えています。サムネイルサイズを上げてください)",
"runSummary": "成功 {generated}、失敗 {failed}、スキップ {skipped}、所要 {seconds} 秒。"
},
"progress": {
"title": "Spineサムネイル",
"generating": "アニメーションサムネイルを生成中……",
"waiting": "開始待ち…",
"confirmPrompt": "キャンセルしますか?",
"resume": "続行",
"cancel": "キャンセル確定",
"stop": "停止",
"confirm": "確認",
"generatedConfirm": "{done} 枚生成しました。キャンセルしますか?",
"complete": "完了",
"completed": "完了 {done} / {total}({percent}%)"
}
}
@@ -0,0 +1,117 @@
{
"manifest": {
"app": {
"name": "Spine 썸네일"
}
},
"thumb": {
"title": "Spine 썸네일",
"noticeIntro": "Spine 에셋(.skel / .json + .atlas + 텍스처)을 가져온 뒤 사용자 지정 썸네일이 없는 항목에 스켈레톤 미리보기를 생성합니다.",
"noticeSafety": "이번에 새로 가져온 항목만 처리합니다. 기존 항목을 검색하거나 수정하지 않으며 사용자 지정 썸네일을 덮어쓰지 않습니다.",
"noticeModes": "애니메이션은 첫 번째 애니메이션을 WebP로 만듭니다. 정지 이미지는 항상 setup pose를 사용합니다.",
"noticeDefault": "자동 생성은 기본적으로 꺼져 있습니다. 필요할 때 아래에서 켜세요.",
"autoToggle": "가져올 때 썸네일 자동 생성",
"clear": "로그 지우기",
"modeLabel": "생성 방식",
"modeAnimated": "애니메이션(첫 번째 애니메이션)",
"modeStatic": "정지 이미지(setup pose)",
"strategyLabel": "생성 전략",
"strategySize": "최소 파일(느림)",
"strategySpeed": "최고 속도(빠름)",
"statusLabel": "상태",
"statusStarting": "시작 중…",
"libraryLabel": "라이브러리",
"statsLabel": "통계",
"filterAll": "전체",
"filterOk": "성공",
"filterFail": "실패",
"filterWarn": "경고",
"filterError": "오류",
"unknownError": "알 수 없는 오류",
"scriptError": "스크립트 오류: {error}",
"promiseError": "처리되지 않은 Promise 오류: {error}",
"uncategorized": "미분류",
"reasonQuiet": "정적 감지",
"reasonFallback": "대체 검색",
"none": "없음",
"complete": "완전",
"incomplete": "불완전",
"statusLoaded": "스크립트 {version}을(를) 불러왔습니다. Eagle 이벤트를 기다리는 중…",
"statusEagleMissing": "Eagle API를 사용할 수 없음",
"statusListening": "감시 중",
"statusListenFailed": "감시 실패",
"statusAutoOff": "감시 중(자동 생성 꺼짐)",
"statusNoLibrary": "라이브러리 경로를 가져올 수 없음",
"statusPairing": "새 에셋 연결 중…",
"statusPairingProgress": "연결 중 {current}/{total}",
"statusRendering": "프레임 렌더링 중 {current}/{total}(대기 {queued})",
"statusGenerating": "썸네일 생성 중… 남은 항목 {queued}",
"statusPaused": "일시 정지됨(진행 창에서 계속하거나 취소하세요)",
"statusAutoSkipped": "자동 생성이 꺼져 있어 이번 묶음을 건너뛰었습니다",
"statusStartFailed": "시작 실패: {error}",
"stats": "묶음 {batches} · 추가 {added} · Spine {spine} · 완전 {complete} · 생성 {generated} · 건너뜀 {skipped} · 실패 {failed}",
"modulesMissing": "로컬 모듈이 로드되지 않음: {modules}",
"moduleLoadError": "로컬 모듈이 로드되지 않았습니다. index.html의 script 순서를 확인하세요: {modules}",
"version": "플러그인 코드 버전 {version}",
"pluginCreated": "플러그인 생성됨: {name}",
"settingsReadFailed": "설정을 읽지 못해 기본값을 사용합니다(자동 생성 꺼짐): {error}",
"settingsSaveFailed": "설정 저장 실패: {error}",
"autoEnabled": "자동 생성을 켰습니다. 새로 가져오는 Spine 에셋에 썸네일을 생성합니다.",
"autoDisabled": "자동 생성을 껐으며 대기 큐를 비웠습니다.",
"modeStaticLog": "생성 방식: 정지 이미지(항상 setup pose, PNG)",
"modeAnimatedLog": "생성 방식: 애니메이션(첫 애니메이션을 WebP로 만들고 없으면 setup pose 사용)",
"strategySpeedLog": "생성 전략: 최고 속도(대략 선택 후 가장 작은 1~2개 프로필만 생성)",
"strategySizeLog": "생성 전략: 최소 파일(10개 프로필을 모두 생성하고 가장 작은 결과 선택)",
"missingItem": "{label} 건너뜀: Eagle에서 항목을 찾을 수 없음",
"suffixFixed": "{label}의 복합 확장자 수정 → {name}",
"suffixFailed": "{label}의 복합 확장자 수정 실패: {error}",
"existingThumbnail": "{label} 건너뜀: 사용자 지정 썸네일이 이미 있음",
"thumbnailRejected": "Eagle이 이 썸네일을 거부했습니다",
"generated": "{label} 생성됨 | {details} | Spine {version}",
"generateFailed": "{label} 생성 실패: {error}",
"cancelled": "생성 취소: 남은 {dropped}개를 버렸습니다. 완료 {done}개(성공 {generated}, 실패 {failed}, 건너뜀 {skipped})",
"batchFailed": "가져오기 묶음 처리 실패: {error}",
"batchFinished": "묶음 완료: {added}개 추가, 라이브러리 전체 {total}개({reason})",
"recheck": "이전 불완전 에셋 {count}개 다시 확인",
"noSpine": "이번 묶음에 Spine 에셋이 없습니다",
"incompletePending": "불완전한 에셋 {count}개를 이후 가져오기 후 다시 확인합니다",
"incompleteIgnored": "불완전한 에셋 {count}개가 있으며 이번 묶음에서는 처리하지 않습니다",
"itemSummary": " {name} | {path} | {state} | 스켈레톤 {skeleton} | atlas {atlas} | 페이지 {pages}(누락 {missing})",
"autoSkipped": "자동 생성이 꺼져 있어 이번 묶음의 에셋 {count}개를 건너뛰었습니다",
"queued": "{added}개를 큐에 추가했습니다. 현재 대기 {queued}개{state}",
"queuedRunning": "(처리 중)",
"libraryInfoNoPath": "library.info()가 경로를 반환하지 않음: {info}",
"libraryInfoFailed": "library.info() 호출 실패: {error}",
"switchingLibrary": "라이브러리 전환: {path}",
"noCreateEvent": "생성 이벤트를 받지 못해 직접 시작합니다",
"watching": "{path} 감시 중(기존 항목 {count}개는 기록으로 간주하여 처리하지 않음)",
"watchReadFailed": "에셋 디렉터리를 읽지 못함: {error}",
"watchMissingDir": "에셋 디렉터리가 없음: {path}",
"watchFailed": "감시 시작 실패: {error}",
"fixSuffixAction": "{label}의 복합 확장자 수정",
"renderAction": "{label} 렌더링",
"setThumbnailAction": "{label} 썸네일 설정",
"thumbnailDetailsAnimated": "애니메이션 {size}@{fps}fps/q{quality}/{kb}KB(목표 {target}KB)/ {frames}프레임 / 애니메이션 {animation}/{strategy}{limit}",
"thumbnailDetailsStatic": "정지 이미지 {width}×{height}/{kb}KB{fallback}",
"bestSpeed": "최고 속도",
"bestSize": "최소 파일",
"fallbackNote": "({text})",
"fallbackNoAnimation": "스켈레톤에 애니메이션이 없습니다",
"fallbackAnimatedFailed": "애니메이션 생성 실패: {error}",
"limitNote": "(가장 작은 프로필도 목표 크기를 초과합니다. 썸네일 크기를 늘리세요)",
"runSummary": "성공 {generated}, 실패 {failed}, 건너뜀 {skipped}, {seconds}초 소요."
},
"progress": {
"title": "Spine 썸네일",
"generating": "애니메이션 썸네일 생성 중……",
"waiting": "시작 대기 중…",
"confirmPrompt": "이번 작업을 취소할까요?",
"resume": "계속",
"cancel": "취소 확인",
"stop": "중지",
"confirm": "확인",
"generatedConfirm": "썸네일 {done}개를 생성했습니다. 취소할까요?",
"complete": "완료",
"completed": "완료 {done} / {total}({percent}%)"
}
}
@@ -0,0 +1,117 @@
{
"manifest": {
"app": {
"name": "Миниатюры Spine"
}
},
"thumb": {
"title": "Миниатюры Spine",
"noticeIntro": "После импорта ресурсов Spine (.skel / .json + .atlas + текстуры) плагин создаёт предпросмотр скелета для элементов без пользовательской миниатюры.",
"noticeSafety": "Обрабатываются только новые элементы. Существующие элементы не сканируются и не изменяются, пользовательские миниатюры никогда не перезаписываются.",
"noticeModes": "Анимированный режим использует первую анимацию и экспортирует WebP. Статический всегда использует setup pose.",
"noticeDefault": "Автоматическое создание по умолчанию отключено. Включите его ниже при необходимости.",
"autoToggle": "Создавать миниатюры при импорте",
"clear": "Очистить журнал",
"modeLabel": "Режим создания",
"modeAnimated": "Анимированный (первая анимация)",
"modeStatic": "Статический (setup pose)",
"strategyLabel": "Стратегия",
"strategySize": "Минимальный файл (медленно)",
"strategySpeed": "Максимальная скорость (быстро)",
"statusLabel": "Состояние",
"statusStarting": "Запуск…",
"libraryLabel": "Библиотека",
"statsLabel": "Статистика",
"filterAll": "Все",
"filterOk": "Успешно",
"filterFail": "Ошибки",
"filterWarn": "Предупреждения",
"filterError": "Ошибка",
"unknownError": "Неизвестная ошибка",
"scriptError": "Ошибка скрипта: {error}",
"promiseError": "Необработанная ошибка Promise: {error}",
"uncategorized": "Без категории",
"reasonQuiet": "после завершения импорта",
"reasonFallback": "резервное сканирование",
"none": "нет",
"complete": "полный",
"incomplete": "неполный",
"statusLoaded": "Скрипт {version} загружен; ожидание событий Eagle…",
"statusEagleMissing": "API Eagle недоступен",
"statusListening": "Наблюдение",
"statusListenFailed": "Ошибка наблюдения",
"statusAutoOff": "Наблюдение (автосоздание выключено)",
"statusNoLibrary": "Путь к библиотеке недоступен",
"statusPairing": "Сопоставление новых ресурсов…",
"statusPairingProgress": "Сопоставление {current}/{total}",
"statusRendering": "Рендеринг кадра {current}/{total} (в очереди {queued})",
"statusGenerating": "Создание миниатюр… осталось {queued}",
"statusPaused": "Пауза (продолжите или отмените в окне прогресса)",
"statusAutoSkipped": "Автосоздание выключено; этот пакет пропущен",
"statusStartFailed": "Ошибка запуска: {error}",
"stats": "Пакеты {batches} · Добавлено {added} · Spine {spine} · Полные {complete} · Создано {generated} · Пропущено {skipped} · Ошибок {failed}",
"modulesMissing": "Локальные модули не загружены: {modules}",
"moduleLoadError": "Локальные модули не загружены. Проверьте порядок скриптов в index.html: {modules}",
"version": "Версия кода плагина {version}",
"pluginCreated": "Плагин создан: {name}",
"settingsReadFailed": "Не удалось прочитать настройки; используются значения по умолчанию (автосоздание выключено): {error}",
"settingsSaveFailed": "Не удалось сохранить настройки: {error}",
"autoEnabled": "Автосоздание включено. Для новых импортированных ресурсов Spine будут созданы миниатюры.",
"autoDisabled": "Автосоздание выключено; очередь ожидания очищена.",
"modeStaticLog": "Режим создания: статический (всегда setup pose, PNG)",
"modeAnimatedLog": "Режим создания: анимированный (первая анимация в WebP; setup pose без анимации)",
"strategySpeedLog": "Стратегия: максимальная скорость (примерный выбор, только один-два наименьших профиля)",
"strategySizeLog": "Стратегия: минимальный файл (все десять профилей и выбор наименьшего)",
"missingItem": "{label} пропущен: элемент не найден в Eagle",
"suffixFixed": "Составное расширение исправлено для {label} → {name}",
"suffixFailed": "Не удалось исправить составное расширение {label}: {error}",
"existingThumbnail": "{label} пропущен: уже есть пользовательская миниатюра",
"thumbnailRejected": "Eagle отклонил эту миниатюру",
"generated": "{label} создан | {details} | Spine {version}",
"generateFailed": "Ошибка создания {label}: {error}",
"cancelled": "Создание отменено: отброшено ожидающих {dropped}. Завершено {done} (успешно {generated}, ошибок {failed}, пропущено {skipped})",
"batchFailed": "Ошибка обработки пакета импорта: {error}",
"batchFinished": "Пакет завершён: добавлено {added}, в библиотеке {total} ({reason})",
"recheck": "Повторная проверка ранее неполных ресурсов: {count}",
"noSpine": "В этом пакете нет ресурсов Spine",
"incompletePending": "Неполных ресурсов: {count}; они будут проверены после следующих импортов",
"incompleteIgnored": "Неполных ресурсов: {count}; в этом пакете они не обрабатываются",
"itemSummary": " {name} | {path} | {state} | скелет {skeleton} | atlas {atlas} | страниц {pages} (не хватает {missing})",
"autoSkipped": "Автосоздание выключено; пропущено ресурсов в пакете: {count}",
"queued": "Добавлено в очередь: {added}; ожидают {queued}{state}",
"queuedRunning": " (обработка)",
"libraryInfoNoPath": "library.info() не вернул путь: {info}",
"libraryInfoFailed": "Ошибка вызова library.info(): {error}",
"switchingLibrary": "Переключение библиотеки: {path}",
"noCreateEvent": "Событие создания не получено; выполняется прямой запуск",
"watching": "Наблюдение за {path} (существующие элементы {count} считаются историей)",
"watchReadFailed": "Не удалось прочитать каталог ресурсов: {error}",
"watchMissingDir": "Каталог ресурсов не существует: {path}",
"watchFailed": "Не удалось начать наблюдение: {error}",
"fixSuffixAction": "Исправление составного расширения {label}",
"renderAction": "Рендеринг {label}",
"setThumbnailAction": "Установка миниатюры {label}",
"thumbnailDetailsAnimated": "Анимация {size}@{fps}fps/q{quality}/{kb}KB (цель {target}KB)/{frames} кадров/анимация {animation}/{strategy}{limit}",
"thumbnailDetailsStatic": "Статическая {width}×{height}/{kb}KB{fallback}",
"bestSpeed": "максимальная скорость",
"bestSize": "минимальный файл",
"fallbackNote": " ({text})",
"fallbackNoAnimation": "в скелете нет анимаций",
"fallbackAnimatedFailed": "ошибка анимированного создания: {error}",
"limitNote": " (даже минимальный профиль превышает целевой размер; увеличьте размер миниатюры)",
"runSummary": "Успешно {generated}, ошибок {failed}, пропущено {skipped}, время {seconds} с."
},
"progress": {
"title": "Миниатюры Spine",
"generating": "Создание анимированных миниатюр…",
"waiting": "Ожидание запуска…",
"confirmPrompt": "Отменить этот запуск?",
"resume": "Продолжить",
"cancel": "Подтвердить отмену",
"stop": "Остановить",
"confirm": "Готово",
"generatedConfirm": "Создано миниатюр: {done}. Отменить запуск?",
"complete": "Завершено",
"completed": "Завершено {done} / {total} ({percent}%)"
}
}
@@ -0,0 +1,117 @@
{
"manifest": {
"app": {
"name": "Spine缩略图"
}
},
"thumb": {
"title": "Spine缩略图",
"noticeIntro": "导入 Spine 资产(.skel / .json + .atlas + 贴图)后,本插件会为还没设置缩略图的素材生成一张骨骼预览图。",
"noticeSafety": "它只处理本次新导入的素材,不扫描、不修改历史素材,也不会覆盖你已经设置过的自定义缩略图。",
"noticeModes": "动态取骨骼里的第一个动画压成 WebP;静帧始终使用 setup pose。",
"noticeDefault": "自动生成默认关闭,需要你在下面手动启用。",
"autoToggle": "导入时自动生成缩略图",
"clear": "清空日志",
"modeLabel": "生成方式",
"modeAnimated": "动态(第一个动画)",
"modeStatic": "静帧(setup pose)",
"strategyLabel": "生成策略",
"strategySize": "最优体积(慢)",
"strategySpeed": "最优速度(快)",
"statusLabel": "状态",
"statusStarting": "启动中…",
"libraryLabel": "素材库",
"statsLabel": "统计",
"filterAll": "全部",
"filterOk": "成功",
"filterFail": "失败",
"filterWarn": "警告",
"filterError": "错误",
"unknownError": "未知错误",
"scriptError": "脚本错误:{error}",
"promiseError": "未处理的 Promise 错误:{error}",
"uncategorized": "未分类",
"reasonQuiet": "静默",
"reasonFallback": "兜底扫描",
"none": "无",
"complete": "完整",
"incomplete": "不完整",
"statusLoaded": "脚本已加载 {version},等待 Eagle 事件…",
"statusEagleMissing": "Eagle API 未注入",
"statusListening": "监听中",
"statusListenFailed": "监听失败",
"statusAutoOff": "监听中(自动生成已关闭)",
"statusNoLibrary": "没有拿到素材库路径",
"statusPairing": "正在配对新增素材…",
"statusPairingProgress": "正在配对 {current}/{total}",
"statusRendering": "正在渲染动画帧 {current}/{total}(排队 {queued} 个)",
"statusGenerating": "正在生成缩略图…剩余 {queued} 个",
"statusPaused": "已暂停(在进度弹窗里继续或取消)",
"statusAutoSkipped": "自动生成未启用,本批已跳过",
"statusStartFailed": "启动失败:{error}",
"stats": "批次 {batches} · 新增 {added} · Spine {spine} · 完整 {complete} · 已生成 {generated} · 跳过 {skipped} · 失败 {failed}",
"modulesMissing": "本地模块未加载:{modules}",
"moduleLoadError": "本地模块未加载,请检查 index.html 里 script 的顺序:{modules}",
"version": "插件代码版本 {version}",
"pluginCreated": "插件已创建:{name}",
"settingsReadFailed": "读取设置失败,按默认值处理(关闭):{error}",
"settingsSaveFailed": "保存设置失败:{error}",
"autoEnabled": "自动生成已启用。之后导入的 Spine 资产会自动生成缩略图。",
"autoDisabled": "自动生成已关闭,待处理队列已清空。",
"modeStaticLog": "生成方式:静帧(始终 setup pose,PNG)",
"modeAnimatedLog": "生成方式:动态(取第一个动画压成 WebP,没有动画时回退 setup pose)",
"strategySpeedLog": "生成策略:最优速度(粗估挑档,只生成最小的一两档)",
"strategySizeLog": "生成策略:最优体积(十档全生成,取实际体积最小的那一档)",
"missingItem": "跳过 {label}:Eagle 里找不到这个素材",
"suffixFixed": "修正复合后缀 {label} → {name}",
"suffixFailed": "修正复合后缀失败 {label}:{error}",
"existingThumbnail": "跳过 {label}:已有自定义缩略图",
"thumbnailRejected": "Eagle 拒绝了这个缩略图",
"generated": "已生成 {label} | {details} | Spine {version}",
"generateFailed": "生成失败 {label}:{error}",
"cancelled": "已取消本轮生成:丢弃剩余 {dropped} 个素材,已完成 {done} 张(成功 {generated} 张,失败 {failed} 张,跳过 {skipped} 张)",
"batchFailed": "处理批次失败:{error}",
"batchFinished": "这批结束:新增 {added} 个,库内共 {total} 个({reason})",
"recheck": "连带重新检查之前 {count} 个配套不全的资产",
"noSpine": "这批里没有 Spine 资产",
"incompletePending": "有 {count} 个资产配套不全,先记为待观察,等后续批次补齐",
"incompleteIgnored": "有 {count} 个资产配套不全,本批不处理",
"itemSummary": " {name} | {path} | {state} | 骨架 {skeleton} | atlas {atlas} | 贴图页 {pages}(缺 {missing})",
"autoSkipped": "自动生成已关闭,本批 {count} 个资产已跳过",
"queued": "已加入队列 {added} 个,当前排队 {queued} 个{state}",
"queuedRunning": "(正在处理中)",
"libraryInfoNoPath": "library.info() 没有返回路径字段:{info}",
"libraryInfoFailed": "library.info() 调用失败:{error}",
"switchingLibrary": "切换素材库:{path}",
"noCreateEvent": "没有收到创建事件,直接尝试启动",
"watching": "监听 {path}(现有 {count} 个素材视为历史,不处理)",
"watchReadFailed": "读取素材目录失败:{error}",
"watchMissingDir": "素材目录不存在:{path}",
"watchFailed": "建立监听失败:{error}",
"fixSuffixAction": "修正复合后缀 {label}",
"renderAction": "渲染 {label}",
"setThumbnailAction": "设置缩略图 {label}",
"thumbnailDetailsAnimated": "动态 {size}@{fps}fps/q{quality}/{kb}kb(目标 {target}kb)/{frames} 帧/动画 {animation}/{strategy}{limit}",
"thumbnailDetailsStatic": "静帧 {width}*{height}/{kb}kb{fallback}",
"bestSpeed": "最优速度",
"bestSize": "最优体积",
"fallbackNote": "({text})",
"fallbackNoAnimation": "骨骼里没有动画",
"fallbackAnimatedFailed": "动态生成失败:{error}",
"limitNote": "(最小档仍超出目标体积,可以考虑提高缩略图大小)",
"runSummary": "成功 {generated} 张,失败 {failed} 张,跳过 {skipped} 张,用时 {seconds} 秒。"
},
"progress": {
"title": "Spine缩略图",
"generating": "正在生成动态缩略图……",
"waiting": "等待开始…",
"confirmPrompt": "是否确认取消?",
"resume": "继续",
"cancel": "确认取消",
"stop": "停止",
"confirm": "确认",
"generatedConfirm": "已生成 {done} 张缩略图,是否确认取消?",
"complete": "已完成",
"completed": "已完成 {done} / {total}({percent}%)"
}
}
@@ -0,0 +1,117 @@
{
"manifest": {
"app": {
"name": "Spine縮圖"
}
},
"thumb": {
"title": "Spine縮圖",
"noticeIntro": "匯入 Spine 資產(.skel / .json + .atlas + 貼圖)後,此外掛會為尚未設定縮圖的素材產生骨骼預覽圖。",
"noticeSafety": "只處理本次新匯入的素材,不掃描或修改歷史素材,也不會覆寫你已設定的自訂縮圖。",
"noticeModes": "動態會取第一個動畫並壓成 WebP;靜態一律使用 setup pose。",
"noticeDefault": "自動產生預設為關閉,需要時請在下方手動啟用。",
"autoToggle": "匯入時自動產生縮圖",
"clear": "清除記錄",
"modeLabel": "產生方式",
"modeAnimated": "動態(第一個動畫)",
"modeStatic": "靜態(setup pose)",
"strategyLabel": "產生策略",
"strategySize": "最小檔案(慢)",
"strategySpeed": "最快速度(快速)",
"statusLabel": "狀態",
"statusStarting": "啟動中…",
"libraryLabel": "素材庫",
"statsLabel": "統計",
"filterAll": "全部",
"filterOk": "成功",
"filterFail": "失敗",
"filterWarn": "警告",
"filterError": "錯誤",
"unknownError": "未知錯誤",
"scriptError": "指令碼錯誤:{error}",
"promiseError": "未處理的 Promise 錯誤:{error}",
"uncategorized": "未分類",
"reasonQuiet": "靜默",
"reasonFallback": "備援掃描",
"none": "無",
"complete": "完整",
"incomplete": "不完整",
"statusLoaded": "指令碼已載入 {version},等待 Eagle 事件…",
"statusEagleMissing": "未注入 Eagle API",
"statusListening": "監聽中",
"statusListenFailed": "監聽失敗",
"statusAutoOff": "監聽中(自動產生已關閉)",
"statusNoLibrary": "無法取得素材庫路徑",
"statusPairing": "正在配對新增素材…",
"statusPairingProgress": "正在配對 {current}/{total}",
"statusRendering": "正在算繪動畫影格 {current}/{total}(佇列 {queued} 個)",
"statusGenerating": "正在產生縮圖…剩餘 {queued} 個",
"statusPaused": "已暫停(請在進度視窗繼續或取消)",
"statusAutoSkipped": "未啟用自動產生,已略過本批素材",
"statusStartFailed": "啟動失敗:{error}",
"stats": "批次 {batches} · 新增 {added} · Spine {spine} · 完整 {complete} · 已產生 {generated} · 略過 {skipped} · 失敗 {failed}",
"modulesMissing": "本機模組未載入:{modules}",
"moduleLoadError": "本機模組未載入,請檢查 index.html 的 script 順序:{modules}",
"version": "外掛程式碼版本 {version}",
"pluginCreated": "已建立外掛:{name}",
"settingsReadFailed": "讀取設定失敗,改用預設值(關閉):{error}",
"settingsSaveFailed": "儲存設定失敗:{error}",
"autoEnabled": "已啟用自動產生。之後匯入的 Spine 資產會自動產生縮圖。",
"autoDisabled": "已關閉自動產生,待處理佇列已清除。",
"modeStaticLog": "產生方式:靜態(一律 setup pose,PNG)",
"modeAnimatedLog": "產生方式:動態(第一個動畫轉為 WebP,沒有動畫時使用 setup pose)",
"strategySpeedLog": "產生策略:最快速度(粗估後只產生最小的一到兩檔)",
"strategySizeLog": "產生策略:最小檔案(產生全部十檔並保留最小結果)",
"missingItem": "略過 {label}:Eagle 中找不到這個素材",
"suffixFixed": "已修正複合副檔名 {label} → {name}",
"suffixFailed": "修正複合副檔名失敗 {label}:{error}",
"existingThumbnail": "略過 {label}:已有自訂縮圖",
"thumbnailRejected": "Eagle 拒絕了這個縮圖",
"generated": "已產生 {label} | {details} | Spine {version}",
"generateFailed": "產生失敗 {label}:{error}",
"cancelled": "已取消本輪產生:丟棄剩餘 {dropped} 個素材,已完成 {done} 張(成功 {generated} 張,失敗 {failed} 張,略過 {skipped} 張)",
"batchFailed": "處理批次失敗:{error}",
"batchFinished": "本批結束:新增 {added} 個,素材庫共 {total} 個({reason})",
"recheck": "重新檢查先前 {count} 個不完整的資產",
"noSpine": "本批沒有 Spine 資產",
"incompletePending": "有 {count} 個資產不完整,將在後續批次重新檢查",
"incompleteIgnored": "有 {count} 個資產不完整,本批未處理",
"itemSummary": " {name} | {path} | {state} | 骨架 {skeleton} | atlas {atlas} | 貼圖頁 {pages}(缺少 {missing})",
"autoSkipped": "自動產生已關閉,本批已略過 {count} 個資產",
"queued": "已加入佇列 {added} 個,目前等待 {queued} 個{state}",
"queuedRunning": "(處理中)",
"libraryInfoNoPath": "library.info() 未傳回路徑:{info}",
"libraryInfoFailed": "library.info() 呼叫失敗:{error}",
"switchingLibrary": "切換素材庫:{path}",
"noCreateEvent": "未收到建立事件,直接嘗試啟動",
"watching": "監聽 {path}(現有 {count} 個素材視為歷史資料,不處理)",
"watchReadFailed": "讀取素材目錄失敗:{error}",
"watchMissingDir": "素材目錄不存在:{path}",
"watchFailed": "建立監聽失敗:{error}",
"fixSuffixAction": "修正複合副檔名 {label}",
"renderAction": "算繪 {label}",
"setThumbnailAction": "設定縮圖 {label}",
"thumbnailDetailsAnimated": "動態 {size}@{fps}fps/q{quality}/{kb}KB(目標 {target}KB)/ {frames} 幀 / 動畫 {animation}/{strategy}{limit}",
"thumbnailDetailsStatic": "靜態 {width}×{height}/{kb}KB{fallback}",
"bestSpeed": "最快速度",
"bestSize": "最小檔案",
"fallbackNote": "({text})",
"fallbackNoAnimation": "骨骼沒有動畫",
"fallbackAnimatedFailed": "動態產生失敗:{error}",
"limitNote": "(最小檔仍超過目標大小,可考慮提高縮圖尺寸)",
"runSummary": "成功 {generated} 張,失敗 {failed} 張,略過 {skipped} 張,耗時 {seconds} 秒。"
},
"progress": {
"title": "Spine縮圖",
"generating": "正在產生動態縮圖……",
"waiting": "等待開始…",
"confirmPrompt": "確定要取消嗎?",
"resume": "繼續",
"cancel": "確認取消",
"stop": "停止",
"confirm": "確認",
"generatedConfirm": "已產生 {done} 張縮圖,確定要取消嗎?",
"complete": "已完成",
"completed": "已完成 {done} / {total}({percent}%)"
}
}
@@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>SpineThumbnailGenerateServer</title>
<title data-i18n="thumb.title">Spine缩略图</title>
<style>
:root { --bg: #1e1e1e; --panel: #252526; --text: #e6e6e6; --dim: #9d9d9d; --accent: #4fc1ff; --border: #3c3c3c; --warn: #e3b341; }
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--text);
@@ -11,6 +11,8 @@
h1 { font-size: 15px; margin: 0; font-weight: 600; text-align: center; }
.notice { color: var(--dim); line-height: 1.6; background: var(--panel);
border: 1px solid var(--border); border-radius: 6px; padding: 9px 11px; }
.notice p { margin: 0 0 6px; }
.notice p:last-child { margin-bottom: 0; }
.notice b { color: var(--text); font-weight: 500; }
.notice code { color: var(--accent); font-family: ui-monospace, Consolas, monospace; }
.toggle-row { display: flex; align-items: center; gap: 8px; cursor: pointer; white-space: nowrap;
@@ -52,56 +54,45 @@
</style>
</head>
<body>
<h1>Spine缩略图</h1>
<h1 data-i18n="thumb.title">Spine缩略图</h1>
<div class="notice">
导入 Spine 资产(<code>.skel</code> / <code>.json</code> + <code>.atlas</code> + 贴图)后,
本插件会为还没设置缩略图的素材生成一张骨骼预览图。
<b>它只处理本次新导入的素材,不扫描、不修改历史素材,也不会覆盖你已经设置过的自定义缩略图。</b>
生成方式可切换:<b>动态</b>取骨骼里的第一个动画压成 WebP(200×200,体积按动画时长自动定,
参数与主插件默认值一致),<b>静帧</b>始终用 setup pose。
自动生成默认关闭,需要你在下面手动启用。
<p data-i18n="thumb.noticeIntro">导入 Spine 资产(.skel / .json + .atlas + 贴图)后,本插件会为还没设置缩略图的素材生成一张骨骼预览图。</p>
<p data-i18n="thumb.noticeSafety">它只处理本次新导入的素材,不扫描、不修改历史素材,也不会覆盖你已经设置过的自定义缩略图。</p>
<p data-i18n="thumb.noticeModes">动态取骨骼里的第一个动画压成 WebP;静帧始终使用 setup pose。</p>
<p data-i18n="thumb.noticeDefault">自动生成默认关闭,需要你在下面手动启用。</p>
</div>
<div class="toolbar">
<div class="toolbar-group">
<label class="toggle-row"><input type="checkbox" id="toggle"><span>导入时自动生成缩略图</span></label>
<button id="clear" type="button">清空日志</button>
<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>
</div>
<div class="toolbar-group">
<label class="mode-row"><span>生成方式</span>
<label class="mode-row"><span data-i18n="thumb.modeLabel">生成方式</span>
<select id="mode">
<option value="animated">动态(第一个动画)</option>
<option value="static">静帧(setup pose)</option>
<option value="animated" data-i18n="thumb.modeAnimated">动态(第一个动画)</option>
<option value="static" data-i18n="thumb.modeStatic">静帧(setup pose)</option>
</select>
</label>
<label class="mode-row"><span>生成策略</span>
<label class="mode-row"><span data-i18n="thumb.strategyLabel">生成策略</span>
<select id="strategy">
<option value="size">最优体积(慢)</option>
<option value="speed">最优速度(快)</option>
<option value="size" data-i18n="thumb.strategySize">最优体积(慢)</option>
<option value="speed" data-i18n="thumb.strategySpeed">最优速度(快)</option>
</select>
</label>
</div>
</div>
<div class="row"><span>状态</span><b id="status">启动中…</b></div>
<div class="row"><span>素材库</span><b id="library">—</b></div>
<div class="row"><span>统计</span><b id="stats">—</b></div>
<div class="row"><span data-i18n="thumb.statusLabel">状态</span><b id="status" data-i18n="thumb.statusStarting">启动中…</b></div>
<div class="row"><span data-i18n="thumb.libraryLabel">素材库</span><b id="library">—</b></div>
<div class="row"><span data-i18n="thumb.statsLabel">统计</span><b id="stats">—</b></div>
<div class="filters" id="filters">
<button type="button" data-filter="all" class="active">全部</button>
<button type="button" data-filter="ok">成功</button>
<button type="button" data-filter="fail">失败</button>
<button type="button" data-filter="warn">警告</button>
<button type="button" data-filter="error">错误</button>
<button type="button" data-filter="all" class="active" data-i18n="thumb.filterAll">全部</button>
<button type="button" data-filter="ok" data-i18n="thumb.filterOk">成功</button>
<button type="button" data-filter="fail" data-i18n="thumb.filterFail">失败</button>
<button type="button" data-filter="warn" data-i18n="thumb.filterWarn">警告</button>
<button type="button" data-filter="error" data-i18n="thumb.filterError">错误</button>
</div>
<ul id="log"></ul>
<script>
// 诊断用:脚本一旦抛错,直接把原因写在状态栏,避免停在「启动中…」看不出问题。
window.addEventListener('error', (event) => {
const el = document.getElementById('status');
if (el) el.textContent = '脚本错误:' + (event.message || event.error || '未知错误');
});
window.addEventListener('unhandledrejection', (event) => {
const el = document.getElementById('status');
if (el) el.textContent = '未处理的 Promise 错误:' + (event.reason && event.reason.message || event.reason);
});
// service 页面里的 require 不以插件目录为基准,本地资源要用绝对路径,这里先把根目录算出来。
(function () {
try {
@@ -114,6 +105,22 @@
}
})();
</script>
<script src="js/i18n.js"></script>
<script>
// 诊断用:脚本一旦抛错,直接把原因写在状态栏,避免停在「启动中…」看不出问题。
window.addEventListener('error', (event) => {
const el = document.getElementById('status');
if (el) el.textContent = SpineThumbI18n.t('thumb.scriptError', {
error: event.message || event.error || SpineThumbI18n.t('thumb.unknownError'),
});
});
window.addEventListener('unhandledrejection', (event) => {
const el = document.getElementById('status');
if (el) el.textContent = SpineThumbI18n.t('thumb.promiseError', {
error: event.reason && event.reason.message || event.reason,
});
});
</script>
<!-- 顺序有依赖:rasterizer 先于 clipping,utils/badge 先于 renderer,plugin.js 最后。 -->
<script src="js/spine-webgl-rasterizer.js"></script>
<script src="js/spine-canvas-clipping.js"></script>
@@ -0,0 +1,111 @@
// Shared localization for the service panel and progress window.
(function () {
'use strict';
const fs = require('fs');
const path = require('path');
const LANGUAGES = ['en', 'zh_CN', 'zh_TW', 'ja_JP', 'ko_KR', 'ru_RU', 'es_ES', 'de_DE'];
const FALLBACK = 'zh_CN';
function normalizeLocale(raw) {
if (!raw) return null;
const tag = String(raw).trim().toLowerCase().replace(/-/g, '_');
if (tag === 'zh_tw' || tag === 'zh_hk' || tag === 'zh_hant' || tag === 'zh_mo') return 'zh_TW';
for (const code of LANGUAGES) {
if (code.toLowerCase() === tag) return code;
}
const base = tag.split('_')[0];
if (base === 'zh') return 'zh_CN';
for (const code of LANGUAGES) {
if (code.toLowerCase().split('_')[0] === base) return code;
}
return null;
}
function requestedLocale() {
try {
const fromUrl = new URLSearchParams(location.search).get('lang');
if (fromUrl) return fromUrl;
} catch (error) { /* No browser location. */ }
try {
if (typeof eagle !== 'undefined' && eagle.app && eagle.app.locale) return eagle.app.locale;
} catch (error) { /* Eagle API is injected asynchronously. */ }
try {
return navigator.language;
} catch (error) {
return null;
}
}
function pluginRoot() {
if (window.SpinePluginPath) return window.SpinePluginPath;
if (window.SpinePluginRoot) return window.SpinePluginRoot;
return typeof __dirname !== 'undefined' ? __dirname : null;
}
function readLocale(code) {
try {
const root = pluginRoot();
if (!root) return {};
return JSON.parse(fs.readFileSync(path.join(root, '_locales', code + '.json'), 'utf8'));
} catch (error) {
return {};
}
}
function lookup(table, key) {
let node = table;
for (const part of String(key).split('.')) {
if (!node || typeof node !== 'object') return undefined;
node = node[part];
}
return typeof node === 'string' ? node : undefined;
}
function apply(root) {
const scope = root || document;
for (const el of scope.querySelectorAll('[data-i18n]')) el.textContent = t(el.dataset.i18n);
for (const el of scope.querySelectorAll('[data-i18n-title]')) el.title = t(el.dataset.i18nTitle);
for (const el of scope.querySelectorAll('[data-i18n-aria-label]')) el.setAttribute('aria-label', t(el.dataset.i18nAriaLabel));
}
let fallbackMessages = readLocale(FALLBACK);
let locale = FALLBACK;
let messages = fallbackMessages;
function t(key, vars) {
let text = lookup(messages, key);
if (text === undefined) text = lookup(fallbackMessages, key);
if (text === undefined) return key;
if (vars) {
text = text.replace(/\{(\w+)\}/g, (whole, name) =>
Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : whole);
}
return text;
}
function setLocale(raw) {
locale = normalizeLocale(raw) || FALLBACK;
fallbackMessages = readLocale(FALLBACK);
messages = locale === FALLBACK ? fallbackMessages : readLocale(locale);
document.documentElement.lang = locale.replace('_', '-');
apply(document);
return locale;
}
const api = {
languages: LANGUAGES,
fallback: FALLBACK,
get locale() { return locale; },
normalizeLocale,
requestedLocale,
setLocale,
t,
apply,
};
window.SpineThumbI18n = api;
setLocale(requestedLocale());
})();
@@ -37,6 +37,7 @@ function cleanupStaleFiles(directory, prefix, maxAgeMs, now = Date.now()) {
function createLibraryWatcher(options) {
const onBatch = options.onBatch || (() => {});
const onLog = options.onLog || (() => {});
const t = options.t || ((key) => key);
let imagesDir = null;
let watcher = null;
let quietTimer = null;
@@ -47,7 +48,7 @@ function createLibraryWatcher(options) {
try {
return new Set(fs.readdirSync(imagesDir).filter((name) => name.endsWith('.info')));
} catch (error) {
onLog('读取素材目录失败:' + ((error && error.message) || error));
onLog(t('thumb.watchReadFailed', { error: (error && error.message) || error }));
return new Set();
}
}
@@ -82,15 +83,15 @@ function createLibraryWatcher(options) {
stop();
imagesDir = path.join(libraryPath, 'images');
if (!fs.existsSync(imagesDir)) {
onLog('素材目录不存在:' + imagesDir);
onLog(t('thumb.watchMissingDir', { path: imagesDir }));
return false;
}
known = listItemDirs();
onLog('监听 ' + imagesDir + '(现有 ' + known.size + ' 个素材视为历史,不处理)');
onLog(t('thumb.watching', { path: imagesDir, count: known.size }));
try {
watcher = fs.watch(imagesDir, { persistent: true }, () => scheduleFlush());
} catch (error) {
onLog('建立监听失败:' + ((error && error.message) || error));
onLog(t('thumb.watchFailed', { error: (error && error.message) || error }));
return false;
}
fallbackTimer = setInterval(() => {
@@ -2,22 +2,26 @@
// 每次改动递增。Eagle 里的 service 插件是启动时加载并常驻的,改完代码必须重新加载插件
// 才会生效;状态栏会显示这个版本号,方便确认加载的到底是不是最新代码。
const VERSION = 'v65';
const VERSION = 'v66';
const path = require('path');
const os = require('os');
const fs = require('fs');
const i18n = window.SpineThumbI18n;
const t = i18n ? i18n.t : (key) => key;
// 本地模块由 index.html 用 <script> 标签按顺序加载后挂在 window 上。
const REQUIRED = [
'SpineThumbI18n',
'SpineThumbLibraryWatcher', 'SpineThumbSpineAssets', 'SpineThumbRenderer', 'SpineThumbSettings',
'SpineUtils', 'SpineBadge', 'SpineVisibleBounds', 'SpineAnimatedProfiles', 'SpineAnimatedWebpEncoder',
'SpineThumbProgress',
];
const missing = REQUIRED.filter((name) => !window[name]);
if (missing.length) {
document.getElementById('status').textContent = '本地模块未加载:' + missing.join(', ');
throw new Error('本地模块未加载,请检查 index.html 里 script 的顺序:' + missing.join(', '));
const modules = missing.join(', ');
document.getElementById('status').textContent = t('thumb.modulesMissing', { modules });
throw new Error(t('thumb.moduleLoadError', { modules }));
}
const { createLibraryWatcher, cleanupStaleFiles } = window.SpineThumbLibraryWatcher;
const { scanSpineItems } = window.SpineThumbSpineAssets;
@@ -54,6 +58,7 @@ let runShowsProgress = false;
// 弹窗暂停期间,队列原地等待,不再取出新的素材。
let paused = false;
let lastControlAt = 0;
let versionLogged = false;
// 「待观察」:本次配套不全的资产记在这里,下一批导入时连带重新检查。
// Eagle 的重复添加提示会把一次导入拆成几批,靠它才能等齐配套文件。
const pending = new Set();
@@ -69,18 +74,18 @@ function log(message, kind) {
console.log('[SpineThumbnailGenerateServer] ' + message);
}
function logVersion() {
if (versionLogged) return;
versionLogged = true;
log(t('thumb.version', { version: VERSION }));
}
function setStatus(text) {
statusEl.textContent = text;
}
function refreshStats() {
statsEl.textContent = '批次 ' + stats.batches +
' · 新增 ' + stats.added +
' · Spine ' + stats.spine +
' · 完整 ' + stats.complete +
' · 已生成 ' + stats.generated +
' · 跳过 ' + stats.skipped +
' · 失败 ' + stats.failed;
statsEl.textContent = t('thumb.stats', stats);
}
function itemIdOf(dirName) {
@@ -111,21 +116,31 @@ function loadFolderPaths() {
function folderPathOf(folderIds) {
if (!folderPaths) loadFolderPaths();
const id = (Array.isArray(folderIds) && folderIds[0]) || null;
return (id && folderPaths.get(id)) || '未分类';
return (id && folderPaths.get(id)) || t('thumb.uncategorized');
}
/** 日志里的渲染结果描述。参数不走本地化,和主插件的候选标签是同一套写法。 */
/** 日志里的渲染结果描述。 */
function describeThumbnail(rendered) {
const kb = Math.max(1, Math.round(rendered.buffer.length / 1024));
if (rendered.mode === 'animated') {
return '动态 ' + rendered.detail + '*' + rendered.detail + '@' + rendered.fps + 'fps/q' +
rendered.quality + '/' + kb + 'kb(目标 ' + rendered.targetKilobytes + 'kb)/' +
rendered.frames + ' 帧/动画 ' + rendered.animation +
'/' + (rendered.strategy === 'speed' ? '最优速度' : '最优体积') +
(rendered.fits === false ? '(最小档仍超出目标体积,可以考虑提高缩略图大小)' : '');
return t('thumb.thumbnailDetailsAnimated', {
size: rendered.detail + '*' + rendered.detail,
fps: rendered.fps,
quality: rendered.quality,
kb,
target: rendered.targetKilobytes,
frames: rendered.frames,
animation: rendered.animation,
strategy: t(rendered.strategy === 'speed' ? 'thumb.bestSpeed' : 'thumb.bestSize'),
limit: rendered.fits === false ? t('thumb.limitNote') : '',
});
}
return '静帧 ' + rendered.width + '*' + rendered.height + '/' + kb + 'kb' +
(rendered.fallback ? '(' + rendered.fallback + ')' : '');
return t('thumb.thumbnailDetailsStatic', {
width: rendered.width,
height: rendered.height,
kb,
fallback: rendered.fallback ? t('thumb.fallbackNote', { text: rendered.fallback }) : '',
});
}
/**
@@ -157,7 +172,7 @@ async function generateOne(item) {
if (!eagleItem) {
stats.failed += 1;
countRun('failed', false, item.entryName);
log('跳过 ' + label + ':Eagle 里找不到这个素材', 'warn');
log(t('thumb.missingItem', { label }), 'warn');
return;
}
// 复合后缀(.skel.bytes / .atlas.txt):Eagle 只按最后一个点拆 name/ext,atlas.txt
@@ -169,17 +184,17 @@ async function generateOne(item) {
try {
eagleItem.name = parts.name;
eagleItem.ext = parts.ext;
await withTimeout(eagleItem.save(), 20000, '修正复合后缀 ' + label);
log('修正复合后缀 ' + label + ' → ' + parts.name + '.' + parts.ext, 'dim');
await withTimeout(eagleItem.save(), 20000, t('thumb.fixSuffixAction', { label }));
log(t('thumb.suffixFixed', { label, name: parts.name + '.' + parts.ext }), 'dim');
} catch (error) {
log('修正复合后缀失败 ' + label + ':' + ((error && error.message) || error), 'warn');
log(t('thumb.suffixFailed', { label, error: (error && error.message) || error }), 'warn');
}
}
// 不覆盖用户已有的自定义缩略图(Eagle 审核要求避免静默覆盖用户数据)。
if (eagleItem.customThumbnail === true) {
stats.skipped += 1;
countRun('skipped', false, item.entryName);
log('跳过 ' + label + ':已有自定义缩略图', 'dim');
log(t('thumb.existingThumbnail', { label }), 'dim');
return;
}
const version = utils.detectVersion(item.skeletonPath);
@@ -197,20 +212,24 @@ async function generateOne(item) {
mode: settings.mode,
strategy: settings.strategy,
onProgress: (current, total) => {
setStatus('正在渲染动画帧 ' + current + '/' + total + '(排队 ' + queue.length + ' 个)');
setStatus(t('thumb.statusRendering', { current, total, queued: queue.length }));
},
}), animated ? 300000 : 60000, '渲染 ' + label);
}), animated ? 300000 : 60000, t('thumb.renderAction', { label }));
tempPath = path.join(os.tmpdir(), 'spine-thumb-' + itemId + '-' + Date.now() + (rendered.extension || '.png'));
fs.writeFileSync(tempPath, rendered.buffer);
const applied = await withTimeout(eagleItem.setCustomThumbnail(tempPath), 20000, '设置缩略图 ' + item.dirName);
if (applied !== true) throw new Error('Eagle 拒绝了这个缩略图');
const applied = await withTimeout(
eagleItem.setCustomThumbnail(tempPath),
20000,
t('thumb.setThumbnailAction', { label: item.dirName }),
);
if (applied !== true) throw new Error(t('thumb.thumbnailRejected'));
stats.generated += 1;
countRun('generated', rendered.mode === 'animated', item.entryName);
log('已生成 ' + label + ' | ' + describeThumbnail(rendered) + ' | Spine ' + version, 'ok');
log(t('thumb.generated', { label, details: describeThumbnail(rendered), version }), 'ok');
} catch (error) {
stats.failed += 1;
countRun('failed', false, item.entryName);
log('生成失败 ' + label + ':' + ((error && error.message) || error), 'fail');
log(t('thumb.generateFailed', { label, error: (error && error.message) || error }), 'fail');
} finally {
try { if (tempPath && fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (error) { /* ignore */ }
refreshStats();
@@ -233,17 +252,20 @@ function applyControl() {
lastControlAt = control.at;
if (control.action === 'pause') {
paused = true;
setStatus('已暂停(在进度弹窗里继续或取消)');
setStatus(t('thumb.statusPaused'));
} else if (control.action === 'resume') {
paused = false;
} else if (control.action === 'cancel') {
paused = false;
const dropped = queue.length;
queue.length = 0;
log('已取消本轮生成:丢弃剩余 ' + dropped + ' 个素材,已完成 ' +
(runStats ? runStats.done : 0) + ' 张(成功 ' + (runStats ? runStats.generated : 0) +
' 张,失败 ' + (runStats ? runStats.failed : 0) +
' 张,跳过 ' + (runStats ? runStats.skipped : 0) + ' 张)', 'warn');
log(t('thumb.cancelled', {
dropped,
done: runStats ? runStats.done : 0,
generated: runStats ? runStats.generated : 0,
failed: runStats ? runStats.failed : 0,
skipped: runStats ? runStats.skipped : 0,
}), 'warn');
}
}
@@ -278,10 +300,10 @@ async function processQueue() {
const item = queue.shift();
// 取消是在上面 applyControl 里生效的,队列可能刚好被清空。
if (!item) break;
setStatus('正在生成缩略图…剩余 ' + queue.length + ' 个');
setStatus(t('thumb.statusGenerating', { queued: queue.length }));
await generateOne(item);
}
setStatus('监听中');
setStatus(t('thumb.statusListening'));
} finally {
running = false;
paused = false;
@@ -298,9 +320,13 @@ async function processQueue() {
/** 在这轮结束时把结果同时写进面板日志和进度弹窗。 */
function finishRun(run) {
const seconds = Math.max(1, Math.round((Date.now() - run.startedAt) / 1000));
const summary = '成功 ' + run.generated + ' 张,失败 ' + run.failed + ' 张,跳过 ' +
run.skipped + ' 张,用时 ' + seconds + ' 秒。';
log('本轮完成:' + summary, 'ok');
const summary = t('thumb.runSummary', {
generated: run.generated,
failed: run.failed,
skipped: run.skipped,
seconds,
});
log(summary, 'ok');
progress.finish(summary);
}
@@ -310,7 +336,7 @@ let batchChain = Promise.resolve();
function handleBatch(batch) {
batchChain = batchChain
.then(() => handleBatchAsync(batch))
.catch((error) => log('处理批次失败:' + ((error && error.message) || error), 'error'));
.catch((error) => log(t('thumb.batchFailed', { error: (error && error.message) || error }), 'error'));
}
/** 一批静默结束后触发:只分析本次新增的目录,避免每次全库扫描。 */
@@ -322,22 +348,23 @@ async function handleBatchAsync({ added, total, reason }) {
loadFolderPaths();
stats.batches += 1;
stats.added += added.length;
log('这批结束:新增 ' + added.length + ' 个,库内共 ' + total + ' 个(' + reason + ')');
const reasonText = t(reason === 'fallback' ? 'thumb.reasonFallback' : 'thumb.reasonQuiet');
log(t('thumb.batchFinished', { added: added.length, total, reason: reasonText }));
// 本次新增 + 之前配套不全的,一起重新检查。
const candidates = [...new Set([...added, ...pending])];
if (pending.size) log('连带重新检查之前 ' + pending.size + ' 个配套不全的资产', 'dim');
if (pending.size) log(t('thumb.recheck', { count: pending.size }), 'dim');
// 先把上面这行日志真正刷到屏幕上,再开始配对——配对是同步扫全库的重活,
// 一旦开始浏览器就没机会重绘,日志会「迟到」到全部结束才一次性出现。
setStatus('正在配对新增素材…');
setStatus(t('thumb.statusPairing'));
await new Promise((resolve) => setTimeout(resolve, 0));
const items = await scanSpineItems(imagesDir, candidates, utils, (current, all) => {
setStatus('正在配对 ' + current + '/' + all);
setStatus(t('thumb.statusPairingProgress', { current, total: all }));
});
// 统计只算本次新增的:待观察的重查会重复计入,数字会虚高。
const fresh = items.filter((item) => added.includes(item.dirName));
stats.spine += fresh.length;
stats.complete += fresh.filter((item) => item.complete).length;
if (!items.length) log('这批里没有 Spine 资产', 'dim');
if (!items.length) log(t('thumb.noSpine'), 'dim');
// 配套不全的先记下来,等下一批补齐时再处理;补齐了就从待观察里移除。
const incomplete = items.filter((item) => !item.complete);
pending.clear();
@@ -347,18 +374,20 @@ async function handleBatchAsync({ added, total, reason }) {
for (const item of incomplete) pending.add(item.dirName);
}
if (incomplete.length) {
log(settings.enabled
? '有 ' + incomplete.length + ' 个资产配套不全,先记为待观察,等后续批次补齐'
: '有 ' + incomplete.length + ' 个资产配套不全,本批不处理',
log(t(settings.enabled ? 'thumb.incompletePending' : 'thumb.incompleteIgnored', { count: incomplete.length }),
settings.enabled ? 'warn' : 'dim');
}
for (const item of items) {
const name = (filePath) => (filePath ? path.basename(filePath) : '无');
log(' ' + item.entryName + ' | ' + folderPathOf(item.folders) + ' | ' +
(item.complete ? '完整' : '不完整') +
'|骨架 ' + name(item.skeletonPath) +
'|atlas ' + name(item.atlasPath) +
'|贴图页 ' + item.pages + '(缺 ' + item.missingTextures + ')',
const name = (filePath) => (filePath ? path.basename(filePath) : t('thumb.none'));
log(t('thumb.itemSummary', {
name: item.entryName,
path: folderPathOf(item.folders),
state: t(item.complete ? 'thumb.complete' : 'thumb.incomplete'),
skeleton: name(item.skeletonPath),
atlas: name(item.atlasPath),
pages: item.pages,
missing: item.missingTextures,
}),
item.complete ? 'ok' : 'warn');
}
refreshStats();
@@ -366,8 +395,8 @@ async function handleBatchAsync({ added, total, reason }) {
const ready = items.filter((item) => item.complete);
if (!ready.length) return;
if (!settings.enabled) {
log('自动生成已关闭,本批 ' + ready.length + ' 个资产已跳过', 'warn');
setStatus('自动生成未启用,本批已跳过');
log(t('thumb.autoSkipped', { count: ready.length }), 'warn');
setStatus(t('thumb.statusAutoSkipped'));
return;
}
queue.push(...ready);
@@ -382,14 +411,15 @@ async function handleBatchAsync({ added, total, reason }) {
total: runStats ? runStats.total : queue.length,
});
}
log('已加入队列 ' + ready.length + ' 个,当前排队 ' + queue.length + ' 个' +
(running ? '(正在处理中)' : ''), 'dim');
log(t('thumb.queued', {
added: ready.length,
queued: queue.length,
state: running ? t('thumb.queuedRunning') : '',
}), 'dim');
processQueue();
}
const libraryWatcher = createLibraryWatcher({ onBatch: handleBatch, onLog: log });
log('插件代码版本 ' + VERSION);
const libraryWatcher = createLibraryWatcher({ onBatch: handleBatch, onLog: log, t });
async function resolveLibraryPath() {
const direct = eagle.library && eagle.library.path;
@@ -398,9 +428,9 @@ async function resolveLibraryPath() {
const info = await eagle.library.info();
const fromApi = info && (info.path || info.libraryPath || info.dir);
if (fromApi) return fromApi;
log('library.info() 没有返回路径字段:' + JSON.stringify(info), 'dim');
log(t('thumb.libraryInfoNoPath', { info: JSON.stringify(info) }), 'dim');
} catch (error) {
log('library.info() 调用失败:' + ((error && error.message) || error), 'error');
log(t('thumb.libraryInfoFailed', { error: (error && error.message) || error }), 'error');
}
return null;
}
@@ -408,21 +438,20 @@ async function resolveLibraryPath() {
async function start() {
const resolved = await resolveLibraryPath();
if (!resolved) {
setStatus('没有拿到素材库路径');
log('切换素材库时会重试', 'warn');
setStatus(t('thumb.statusNoLibrary'));
return;
}
libraryPath = resolved;
imagesDir = path.join(resolved, 'images');
folderPaths = null; // 换库了,文件夹树重新读
libraryEl.textContent = resolved;
setStatus(libraryWatcher.start(resolved) ? '监听中' : '监听失败');
setStatus(libraryWatcher.start(resolved) ? t('thumb.statusListening') : t('thumb.statusListenFailed'));
}
function startSafely() {
start().catch((error) => {
setStatus('启动失败:' + ((error && error.message) || error));
log('启动失败:' + ((error && error.stack) || error), 'error');
setStatus(t('thumb.statusStartFailed', { error: (error && error.message) || error }));
log(t('thumb.statusStartFailed', { error: (error && error.stack) || error }), 'error');
});
}
@@ -430,7 +459,7 @@ async function loadSettings() {
try {
settings = await settingsStore.read();
} catch (error) {
log('读取设置失败,按默认值处理(关闭):' + ((error && error.message) || error), 'warn');
log(t('thumb.settingsReadFailed', { error: (error && error.message) || error }), 'warn');
settings = { ...settingsStore.DEFAULTS };
}
toggleEl.checked = settings.enabled === true;
@@ -443,15 +472,15 @@ toggleEl.addEventListener('change', async () => {
try {
await settingsStore.write(settings);
} catch (error) {
log('保存设置失败:' + ((error && error.message) || error), 'warn');
log(t('thumb.settingsSaveFailed', { error: (error && error.message) || error }), 'warn');
}
if (settings.enabled) {
log('自动生成已启用。之后导入的 Spine 资产会自动生成缩略图。', 'ok');
setStatus('监听中');
log(t('thumb.autoEnabled'), 'ok');
setStatus(t('thumb.statusListening'));
} else {
queue.length = 0;
log('自动生成已关闭,待处理队列已清空。', 'dim');
setStatus('监听中(自动生成已关闭)');
log(t('thumb.autoDisabled'), 'dim');
setStatus(t('thumb.statusAutoOff'));
}
});
@@ -465,11 +494,9 @@ modeEl.addEventListener('change', async () => {
try {
await settingsStore.write(settings);
} catch (error) {
log('保存设置失败:' + ((error && error.message) || error), 'warn');
log(t('thumb.settingsSaveFailed', { error: (error && error.message) || error }), 'warn');
}
log(settings.mode === 'static'
? '生成方式:静帧(始终 setup pose,PNG)'
: '生成方式:动态(取第一个动画压成 WebP,没有动画时回退 setup pose)', 'dim');
log(t(settings.mode === 'static' ? 'thumb.modeStaticLog' : 'thumb.modeAnimatedLog'), 'dim');
});
strategyEl.addEventListener('change', async () => {
@@ -477,11 +504,9 @@ strategyEl.addEventListener('change', async () => {
try {
await settingsStore.write(settings);
} catch (error) {
log('保存设置失败:' + ((error && error.message) || error), 'warn');
log(t('thumb.settingsSaveFailed', { error: (error && error.message) || error }), 'warn');
}
log(settings.strategy === 'speed'
? '生成策略:最优速度(粗估挑档,只生成最小的一两档)'
: '生成策略:最优体积(十档全生成,取实际体积最小的那一档)', 'dim');
log(t(settings.strategy === 'speed' ? 'thumb.strategySpeedLog' : 'thumb.strategySizeLog'), 'dim');
});
// 日志分类过滤交给 CSS:这里只切 #log 的 data-filter 和按钮高亮。
@@ -493,33 +518,39 @@ filtersEl.addEventListener('click', (event) => {
});
// 先确认脚本本身执行到了这里,再谈 Eagle 事件有没有来。
document.getElementById('status').textContent = '脚本已加载 ' + VERSION + ',等待 Eagle 事件…';
document.getElementById('status').textContent = t('thumb.statusLoaded', { version: VERSION });
cleanupStaleFiles(os.tmpdir(), 'spine-thumb-', STALE_TEMP_AGE_MS);
if (typeof eagle === 'undefined') {
setStatus('eagle API 未注入');
log('页面上没有 eagle 对象,插件无法与 Eagle 通信', 'error');
setStatus(t('thumb.statusEagleMissing'));
log(t('thumb.statusEagleMissing'), 'error');
} else {
eagle.onPluginCreate(async (plugin) => {
// 官方文档:onPluginCreate 用来初始化插件所需的模块,plugin.path 就是插件所在目录。
// app.getPath 这类 API 必须在这个事件之后才能调用,所以设置读取也放在这里。
window.SpinePluginPath = (plugin && plugin.path) || window.SpinePluginRoot;
log('插件已创建:' + ((plugin && plugin.manifest && plugin.manifest.name) || 'SpineThumbnailGenerateServer'));
if (i18n) i18n.setLocale(eagle.app && eagle.app.locale);
logVersion();
log(t('thumb.pluginCreated', {
name: (plugin && plugin.manifest && plugin.manifest.name) || t('thumb.title'),
}));
try {
await loadSettings();
} catch (error) {
log('读取设置失败,按默认值处理(关闭):' + ((error && error.message) || error), 'warn');
log(t('thumb.settingsReadFailed', { error: (error && error.message) || error }), 'warn');
}
startSafely();
});
eagle.onLibraryChanged((changedPath) => {
log('切换素材库:' + changedPath);
log(t('thumb.switchingLibrary', { path: changedPath }));
startSafely();
});
// 兜底:service 插件可能在窗口打开前就已创建完,创建事件错过了就自己启动一次。
setTimeout(() => {
if (libraryEl.textContent === '—') {
log('没有收到创建事件,直接尝试启动', 'dim');
if (i18n) i18n.setLocale(i18n.requestedLocale());
logVersion();
log(t('thumb.noCreateEvent'), 'dim');
loadSettings().catch(() => {}).then(startSafely);
}
}, 800);
@@ -13,6 +13,8 @@
const fs = require('fs');
const os = require('os');
const path = require('path');
const i18n = window.SpineThumbI18n;
const t = i18n ? i18n.t : ((key) => key);
const STATE_FILE = path.join(os.tmpdir(), 'spine-thumbnail-progress.json');
const ALIVE_FILE = path.join(os.tmpdir(), 'spine-thumbnail-progress-alive');
@@ -68,7 +70,7 @@ function createPopup() {
// 正常情况下 eagle.plugin 里就带 manifest;万一还没注入就自己从插件目录读一份。
manifest = JSON.parse(fs.readFileSync(path.join(dir, 'manifest.json'), 'utf8'));
}
if (!manifest) return Promise.reject(new Error('拿不到插件 manifest'));
if (!manifest) return Promise.reject(new Error(t('thumb.modulesMissing', { modules: 'manifest' })));
return getIpcRenderer().invoke('create-plugin-window', {
plugin: {
...plugin,
@@ -76,12 +78,12 @@ function createPopup() {
manifest: {
...manifest,
// 这个窗口的标题栏文字(只影响这扇窗,插件本身的名字不变)。
name: 'Spine缩略图',
name: t('progress.title'),
main: {
...manifest.main,
// 必须关掉 serviceMode,否则 Eagle 会按 show:false 创建这个窗口。
serviceMode: false,
url: 'progress.html',
url: 'progress.html?lang=' + encodeURIComponent(i18n ? i18n.locale : 'zh_CN'),
width: WINDOW_SIZE.width,
height: WINDOW_SIZE.height,
show: true,
@@ -10,6 +10,7 @@
const fs = require('fs');
const path = require('path');
const { pathToFileURL } = require('url');
const t = (key, vars) => window.SpineThumbI18n ? window.SpineThumbI18n.t(key, vars) : key;
const runtimeCache = new Map();
// 长动画不截断时间轴,只降低采样密度:18 fps 采样,最多 240 帧。
@@ -678,10 +679,10 @@ async function renderThumbnailAuto(options) {
try {
const animated = await renderAnimatedThumbnail(options);
if (animated) return animated;
return { ...(await renderStillBuffer(options)), fallback: '骨骼里没有动画' };
return { ...(await renderStillBuffer(options)), fallback: t('thumb.fallbackNoAnimation') };
} catch (error) {
const still = await renderStillBuffer(options);
return { ...still, fallback: '动态生成失败:' + ((error && error.message) || error) };
return { ...still, fallback: t('thumb.fallbackAnimatedFailed', { error: (error && error.message) || error }) };
}
}
@@ -3,13 +3,24 @@
"version": "0.1.0",
"platform": "all",
"arch": "all",
"name": "SpineThumbnailGenerateServer",
"name": "{{manifest.app.name}}",
"logo": "/logo.png",
"keywords": [
"spine",
"thumbnail",
"缩略图"
],
"fallbackLanguage": "zh_CN",
"languages": [
"zh_CN",
"en",
"zh_TW",
"ja_JP",
"ko_KR",
"ru_RU",
"es_ES",
"de_DE"
],
"devTools": false,
"main": {
"serviceMode": true,
@@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Spine缩略图</title>
<title data-i18n="progress.title">Spine缩略图</title>
<style>
:root { --bg: #1e1e1e; --panel: #252526; --text: #e6e6e6; --dim: #9d9d9d; --accent: #4fc1ff; --border: #3c3c3c; }
html, body { margin: 0; background: var(--bg); color: var(--text);
@@ -29,18 +29,29 @@
</style>
</head>
<body>
<h1>正在生成动态缩略图……</h1>
<h1 data-i18n="progress.generating">正在生成动态缩略图……</h1>
<div class="bar"><i id="bar"></i></div>
<div id="count">等待开始…</div>
<div id="count" data-i18n="progress.waiting">等待开始…</div>
<div id="name"></div>
<div id="confirm">
<div id="confirmText">是否确认取消?</div>
<div id="confirmText" data-i18n="progress.confirmPrompt">是否确认取消?</div>
<div id="confirmButtons">
<button id="resume" type="button">继续</button>
<button id="cancel" type="button">确认取消</button>
<button id="resume" type="button" data-i18n="progress.resume">继续</button>
<button id="cancel" type="button" data-i18n="progress.cancel">确认取消</button>
</div>
</div>
<button id="action" type="button">停止</button>
<button id="action" type="button" data-i18n="progress.stop">停止</button>
<script>
(function () {
try {
const pathname = decodeURIComponent(new URL('.', location.href).pathname).replace(/\\/g, '/');
window.SpinePluginRoot = pathname.replace(/^\/([A-Za-z]:)/, '$1');
} catch (error) {
window.SpinePluginRoot = '';
}
})();
</script>
<script src="js/i18n.js"></script>
<script>
// 这个页面是宿主用 Eagle 的 create-plugin-window 打开的插件窗口:
// 无标题栏、置顶、挂在 Eagle 主窗口上;进度从临时目录的状态文件轮询。
@@ -52,6 +63,7 @@
const ALIVE_FILE = path.join(os.tmpdir(), 'spine-thumbnail-progress-alive');
const CONTROL_FILE = path.join(os.tmpdir(), 'spine-thumbnail-progress-control.json');
const WIDTH = 380;
const t = SpineThumbI18n.t;
const bar = document.getElementById('bar');
const countEl = document.getElementById('count');
const nameEl = document.getElementById('name');
@@ -117,7 +129,7 @@
/** 三种状态:生成中显示「停止」,暂停询问时显示「继续 / 取消」,跑完显示「确认」。 */
function syncButtons() {
actionEl.textContent = finished ? '确认' : '停止';
actionEl.textContent = finished ? t('progress.confirm') : t('progress.stop');
actionEl.style.display = paused ? 'none' : '';
confirmEl.style.display = paused ? 'flex' : 'none';
fitHeight();
@@ -151,15 +163,18 @@
finished = state.finished === true;
if (finished) paused = false;
doneCount = done;
confirmTextEl.textContent = '已生成 ' + doneCount + ' 张缩略图,是否确认取消?';
confirmTextEl.textContent = t('progress.generatedConfirm', { done: doneCount });
if (finished) {
bar.style.width = '100%';
countEl.textContent = state.text || '已完成';
countEl.textContent = state.text || t('progress.complete');
nameEl.textContent = '';
} else {
bar.style.width = Math.min(100, done / total * 100) + '%';
countEl.textContent = '已完成 ' + done + ' / ' + (state.total || 0) +
'(' + Math.round(done / total * 100) + '%)';
countEl.textContent = t('progress.completed', {
done,
total: state.total || 0,
percent: Math.round(done / total * 100),
});
nameEl.textContent = state.name || '';
}
syncButtons();