八国语言+eagle图片
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();
|
||||
|
||||
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 208 KiB |
|
After Width: | Height: | Size: 660 KiB |
|
After Width: | Height: | Size: 286 KiB |
|
After Width: | Height: | Size: 227 KiB |
|
After Width: | Height: | Size: 668 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 658 KiB |
|
After Width: | Height: | Size: 298 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 665 KiB |
@@ -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();
|
||||
|
||||