diff --git a/README.md b/README.md index a9d7406..decd3c3 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ SpineViewer/ 1. **跨文件夹资源配对**:Eagle 会把导入的每个文件拆分成独立的元数据文件夹(`images/.info/`,内含资产文件与 `metadata.json`),因此配套文件可能不在同一目录。插件会: - 定位到所选文件的 item 文件夹; - - 扫描其**同级的所有 item 文件夹**,根据文件名(忽略 `-pro`、`-ess`、`-pma` 等常见后缀)自动关联出配套的 **骨骼文件** + **atlas** + **贴图**; + - 涋描其**同级的所有 item 文件夹**,根据文件名(忽略 `-pro`、`-ess`、`-pma`、`-base`、`-atlas`、`-data`、`-mesh` 等常见尾缀,`-` 与 `_` 均可)自动关联出配套的 **骨骼文件** + **atlas** + **贴图**;尾缀表可在预览顶栏的“尾缀”下拉里追加自定义项; - 从 atlas 中解析它引用的贴图页面,再到库中定位实际贴图文件。 2. **版本检测**:读取骨骼数据里的版本号(`.json` 读 `skeleton.spine`,`.skel` 读二进制头部),自动选择匹配的本地运行时。 3. **预览**:查看器通过 Node `fs` 直接读取分离在各 item 文件夹中的骨骼/atlas/贴图文件(无需网络、无 CORS),用 `spine-canvas` 渲染器在 `` 上实时播放动画,并支持: @@ -63,6 +63,7 @@ SpineViewer/ - 零时长动画按单帧静态姿势预览 - 自动适配并居中 - 用户可主动将当前帧设为当前素材的自定义缩略图;若已有自定义缩略图,替换前会明确确认 + - 顶栏“尾缀”下拉列出内置尾缀(`pro` `ess` `pma` `base` `atlas` `data` `mesh`)与用户自定义尾缀;选择“+ 添加尾缀…”可新增(仅字母、数字、`-`、`_`,最长 50 字符,上限由 `js/spine-utils.js` 的 `SUFFIX_MAX_LENGTH` 统一定义),选择自定义项可移除。配置保存在 Eagle userData 的 `SpineViewer/suffix-settings.json`,对全部素材与资源库生效,修改后自动重新加载以重新配对 4. **缩略图**:导入时不运行插件缩略图生成器,由 Eagle 自身处理。用户打开 Spine 预览后,仍可用原有按钮将当前帧设为**当前素材**的自定义缩略图。左侧的“为关联资源生成缩略图”会先列出已验证的同 stem `json / skel / skel.bytes / atlas / atlas.txt` 和 atlas 引用贴图;骨骼与 atlas 默认勾选,图片和已有自定义缩略图的素材默认不勾选;用户可主动勾选已有自定义缩略图的素材,确认后覆盖,原图无法由插件自动恢复。手动缩略图会添加 Spine badge 和导出版本号。 --- diff --git a/_locales/de_DE.json b/_locales/de_DE.json index 88e962e..a249ad9 100644 --- a/_locales/de_DE.json +++ b/_locales/de_DE.json @@ -91,6 +91,22 @@ "loadFailed": "Laden fehlgeschlagen", "notSpineText": "Diese Datei ist keine Spine-Ressource im Format .atlas.txt oder .skel.bytes.", "truncated256": "\n\n(nur die ersten 256 KB werden angezeigt)", + "suffix": "Suffix", + "suffixTitle": "Dateinamens-Suffixe werden beim Abgleich ignoriert, daher wird hero-mesh.skel mit hero.atlas verknüpft", + "suffixManage": "Suffixe verwalten…", + "suffixAdd": "+ Suffix hinzufügen…", + "suffixDefaults": "Integrierte Suffixe", + "suffixCustom": "Eigene Suffixe (Auswahl entfernt)", + "suffixAddTitle": "Suffix hinzufügen", + "suffixAddNote": "Suffixe werden beim Dateiabgleich ignoriert: Nach dem Hinzufügen von hover wird hero-hover.skel mit hero.atlas verknüpft. Nur Buchstaben, Ziffern, - und _, maximal 50 Zeichen.", + "suffixPlaceholder": "z. B. hover", + "suffixInvalid": "Nur Buchstaben, Ziffern, - und _, beginnend mit Buchstabe oder Ziffer", + "suffixDuplicate": "Dieses Suffix existiert bereits", + "suffixConfirm": "Hinzufügen", + "suffixRemoveTitle": "Suffix entfernen", + "suffixRemoveNote": "Nach dem Entfernen von „{name}“ werden Dateien mit diesem Suffix nicht mehr automatisch verknüpft.", + "suffixRemove": "Entfernen", + "suffixSaveFailed": "Speichern der Suffix-Liste fehlgeschlagen: {error}", "truncated1m": "\n\n(große Datei; nur das erste 1 MB wird angezeigt)", "groupWillReplaceCustom": "Ersetzt vorhandenes eigenes Vorschaubild", "groupCustomNotSelected": "Eigenes Vorschaubild vorhanden (nicht ausgewählt)" diff --git a/_locales/en.json b/_locales/en.json index 60950a5..823f1d4 100644 --- a/_locales/en.json +++ b/_locales/en.json @@ -93,6 +93,22 @@ "loadFailed": "Failed to load", "notSpineText": "This file is not a .atlas.txt or .skel.bytes Spine asset.", "truncated256": "\n\n(only the first 256 KB is shown)", + "suffix": "Suffix", + "suffixTitle": "Filename suffixes are ignored when matching, so hero-mesh.skel pairs with hero.atlas", + "suffixManage": "Manage suffixes…", + "suffixAdd": "+ Add suffix…", + "suffixDefaults": "Built-in suffixes", + "suffixCustom": "Custom suffixes (select to remove)", + "suffixAddTitle": "Add a suffix", + "suffixAddNote": "Suffixes are ignored when matching filenames: after adding hover, hero-hover.skel pairs with hero.atlas. Letters, digits, - and _ only, up to 50 characters.", + "suffixPlaceholder": "e.g. hover", + "suffixInvalid": "Use letters, digits, - and _ only, and start with a letter or digit", + "suffixDuplicate": "That suffix already exists", + "suffixConfirm": "Add", + "suffixRemoveTitle": "Remove a suffix", + "suffixRemoveNote": "After removing “{name}”, files carrying that suffix will no longer be paired automatically.", + "suffixRemove": "Remove", + "suffixSaveFailed": "Saving the suffix list failed: {error}", "truncated1m": "\n\n(large file, only the first 1 MB is shown)" } } diff --git a/_locales/es_ES.json b/_locales/es_ES.json index e53a8ba..37d57a5 100644 --- a/_locales/es_ES.json +++ b/_locales/es_ES.json @@ -91,6 +91,22 @@ "loadFailed": "Error al cargar", "notSpineText": "Este archivo no es un recurso Spine en formato .atlas.txt o .skel.bytes.", "truncated256": "\n\n(solo se muestran los primeros 256 KB)", + "suffix": "Sufijo", + "suffixTitle": "Los sufijos del nombre se ignoran al emparejar, de modo que hero-mesh.skel se une con hero.atlas", + "suffixManage": "Gestionar sufijos…", + "suffixAdd": "+ Añadir sufijo…", + "suffixDefaults": "Sufijos integrados", + "suffixCustom": "Sufijos personalizados (selecciona para quitar)", + "suffixAddTitle": "Añadir un sufijo", + "suffixAddNote": "Los sufijos se ignoran al emparejar nombres: al añadir hover, hero-hover.skel se empareja con hero.atlas. Solo letras, dígitos, - y _, hasta 50 caracteres.", + "suffixPlaceholder": "p. ej. hover", + "suffixInvalid": "Usa solo letras, dígitos, - y _, y empieza por letra o dígito", + "suffixDuplicate": "Ese sufijo ya existe", + "suffixConfirm": "Añadir", + "suffixRemoveTitle": "Quitar un sufijo", + "suffixRemoveNote": "Tras quitar «{name}», los archivos con ese sufijo dejarán de emparejarse automáticamente.", + "suffixRemove": "Quitar", + "suffixSaveFailed": "No se pudo guardar la lista de sufijos: {error}", "truncated1m": "\n\n(archivo grande; solo se muestra el primer 1 MB)", "groupWillReplaceCustom": "Reemplazará la miniatura personalizada", "groupCustomNotSelected": "Tiene miniatura personalizada (no seleccionado)" diff --git a/_locales/ja_JP.json b/_locales/ja_JP.json index 841e1b1..e9d11e1 100644 --- a/_locales/ja_JP.json +++ b/_locales/ja_JP.json @@ -91,6 +91,22 @@ "loadFailed": "読み込みに失敗しました", "notSpineText": "このファイルは .atlas.txt または .skel.bytes 形式の Spine リソースではありません。", "truncated256": "\n\n(先頭 256 KB のみ表示)", + "suffix": "接尾辞", + "suffixTitle": "ファイル名の接尾辞は照合時に無視され、hero-mesh.skel と hero.atlas が同じセットとして扱われます", + "suffixManage": "接尾辞を管理…", + "suffixAdd": "+ 接尾辞を追加…", + "suffixDefaults": "組み込みの接尾辞", + "suffixCustom": "カスタム接尾辞(選択で削除)", + "suffixAddTitle": "接尾辞を追加", + "suffixAddNote": "接尾辞はファイル名の照合時に無視されます。hover を追加すると hero-hover.skel は hero.atlas と同じセットになります。使用できるのは英数字と - _ のみ、最大 50 文字です。", + "suffixPlaceholder": "例: hover", + "suffixInvalid": "英数字と - _ のみ使用でき、先頭は英数字である必要があります", + "suffixDuplicate": "その接尾辞は既に存在します", + "suffixConfirm": "追加", + "suffixRemoveTitle": "接尾辞を削除", + "suffixRemoveNote": "「{name}」を削除すると、その接尾辞を含むファイルは自動照合されなくなります。", + "suffixRemove": "削除", + "suffixSaveFailed": "接尾辞設定の保存に失敗しました: {error}", "truncated1m": "\n\n(ファイルが大きいため、先頭 1 MB のみ表示)", "groupWillReplaceCustom": "既存のカスタムサムネイルを置き換えます", "groupCustomNotSelected": "カスタムサムネイルあり(未選択)" diff --git a/_locales/ko_KR.json b/_locales/ko_KR.json index 2ad05f1..0c6ea1d 100644 --- a/_locales/ko_KR.json +++ b/_locales/ko_KR.json @@ -91,6 +91,22 @@ "loadFailed": "불러오기 실패", "notSpineText": "이 파일은 .atlas.txt 또는 .skel.bytes 형식의 Spine 리소스가 아닙니다.", "truncated256": "\n\n(앞부분 256 KB만 표시)", + "suffix": "접미사", + "suffixTitle": "파일 이름 접미사는 매칭 시 무시되어 hero-mesh.skel 이 hero.atlas 와 같은 세트로 묶입니다", + "suffixManage": "접미사 관리…", + "suffixAdd": "+ 접미사 추가…", + "suffixDefaults": "기본 접미사", + "suffixCustom": "사용자 접미사(선택하면 제거)", + "suffixAddTitle": "접미사 추가", + "suffixAddNote": "접미사는 파일 이름 매칭 시 무시됩니다. hover 를 추가하면 hero-hover.skel 이 hero.atlas 와 같은 세트가 됩니다. 영문, 숫자, - 와 _ 만 사용할 수 있으며 최대 50자입니다.", + "suffixPlaceholder": "예: hover", + "suffixInvalid": "영문, 숫자, - 와 _ 만 사용할 수 있으며 첫 글자는 영문이나 숫자여야 합니다", + "suffixDuplicate": "이미 존재하는 접미사입니다", + "suffixConfirm": "추가", + "suffixRemoveTitle": "접미사 제거", + "suffixRemoveNote": "“{name}”을(를) 제거하면 해당 접미사가 붙은 파일은 더 이상 자동으로 매칭되지 않습니다.", + "suffixRemove": "제거", + "suffixSaveFailed": "접미사 설정을 저장하지 못했습니다: {error}", "truncated1m": "\n\n(파일이 커서 앞부분 1 MB만 표시)", "groupWillReplaceCustom": "기존 사용자 지정 썸네일 교체", "groupCustomNotSelected": "사용자 지정 썸네일 있음(선택 안 함)" diff --git a/_locales/ru_RU.json b/_locales/ru_RU.json index 074ab2a..f0ceafc 100644 --- a/_locales/ru_RU.json +++ b/_locales/ru_RU.json @@ -91,6 +91,22 @@ "loadFailed": "Не удалось загрузить", "notSpineText": "Этот файл не является ресурсом Spine в формате .atlas.txt или .skel.bytes.", "truncated256": "\n\n(показаны только первые 256 КБ)", + "suffix": "Суффикс", + "suffixTitle": "Суффиксы имён файлов игнорируются при сопоставлении, поэтому hero-mesh.skel объединяется с hero.atlas", + "suffixManage": "Управление суффиксами…", + "suffixAdd": "+ Добавить суффикс…", + "suffixDefaults": "Встроенные суффиксы", + "suffixCustom": "Пользовательские суффиксы (выбор — удалить)", + "suffixAddTitle": "Добавить суффикс", + "suffixAddNote": "Суффиксы игнорируются при сопоставлении имён: добавив hover, файл hero-hover.skel объединится с hero.atlas. Допустимы буквы, цифры, - и _, не более 50 символов.", + "suffixPlaceholder": "например hover", + "suffixInvalid": "Допустимы только буквы, цифры, - и _; первый символ — буква или цифра", + "suffixDuplicate": "Такой суффикс уже есть", + "suffixConfirm": "Добавить", + "suffixRemoveTitle": "Удалить суффикс", + "suffixRemoveNote": "После удаления «{name}» файлы с этим суффиксом больше не будут сопоставляться автоматически.", + "suffixRemove": "Удалить", + "suffixSaveFailed": "Не удалось сохранить список суффиксов: {error}", "truncated1m": "\n\n(файл большой, показан только первый 1 МБ)", "groupWillReplaceCustom": "Заменит существующую миниатюру", "groupCustomNotSelected": "Есть своя миниатюра (не выбрано)" diff --git a/_locales/zh_CN.json b/_locales/zh_CN.json index d9ff0f2..469b367 100644 --- a/_locales/zh_CN.json +++ b/_locales/zh_CN.json @@ -93,6 +93,22 @@ "loadFailed": "加载失败", "notSpineText": "此文件不是 .atlas.txt 或 .skel.bytes 格式的 Spine 资源。", "truncated256": "\n\n(仅显示前 256 KB)", + "suffix": "尾缀", + "suffixTitle": "文件名尾缀在匹配时会被忽略,使 hero-mesh.skel 与 hero.atlas 归为同一套资源", + "suffixManage": "管理尾缀…", + "suffixAdd": "+ 添加尾缀…", + "suffixDefaults": "内置尾缀", + "suffixCustom": "自定义尾缀(选中即移除)", + "suffixAddTitle": "添加尾缀", + "suffixAddNote": "尾缀会在文件名匹配时被忽略:添加 hover 后,hero-hover.skel 会与 hero.atlas 归为同一套资源。只能使用字母、数字、- 和 _,最长 50 个字符。", + "suffixPlaceholder": "例如 hover", + "suffixInvalid": "只能使用字母、数字、- 和 _,且需以字母或数字开头", + "suffixDuplicate": "该尾缀已存在", + "suffixConfirm": "添加", + "suffixRemoveTitle": "移除尾缀", + "suffixRemoveNote": "移除“{name}”后,带该尾缀的文件将不再自动配对。", + "suffixRemove": "移除", + "suffixSaveFailed": "保存尾缀设置失败:{error}", "truncated1m": "\n\n(文件较大,仅显示前 1 MB)" } } diff --git a/_locales/zh_TW.json b/_locales/zh_TW.json index 6683cdb..7155422 100644 --- a/_locales/zh_TW.json +++ b/_locales/zh_TW.json @@ -93,6 +93,22 @@ "loadFailed": "載入失敗", "notSpineText": "此檔案不是 .atlas.txt 或 .skel.bytes 格式的 Spine 資源。", "truncated256": "\n\n(僅顯示前 256 KB)", + "suffix": "尾綴", + "suffixTitle": "檔名尾綴在比對時會被忽略,讓 hero-mesh.skel 與 hero.atlas 歸為同一套資源", + "suffixManage": "管理尾綴…", + "suffixAdd": "+ 新增尾綴…", + "suffixDefaults": "內建尾綴", + "suffixCustom": "自訂尾綴(選取即移除)", + "suffixAddTitle": "新增尾綴", + "suffixAddNote": "尾綴會在檔名比對時被忽略:新增 hover 後,hero-hover.skel 會與 hero.atlas 歸為同一套資源。僅能使用字母、數字、- 和 _,最長 50 個字元。", + "suffixPlaceholder": "例如 hover", + "suffixInvalid": "僅能使用字母、數字、- 和 _,且需以字母或數字開頭", + "suffixDuplicate": "該尾綴已存在", + "suffixConfirm": "新增", + "suffixRemoveTitle": "移除尾綴", + "suffixRemoveNote": "移除「{name}」後,帶該尾綴的檔案將不再自動配對。", + "suffixRemove": "移除", + "suffixSaveFailed": "儲存尾綴設定失敗:{error}", "truncated1m": "\n\n(檔案較大,僅顯示前 1 MB)" } } diff --git a/js/spine-preview-settings.js b/js/spine-preview-settings.js index 93b6e87..dc7a3bb 100644 --- a/js/spine-preview-settings.js +++ b/js/spine-preview-settings.js @@ -55,4 +55,35 @@ async function resolveDataPath(app) { return path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'Eagle'); } -module.exports = { createStore, resolveDataPath }; +// 尾缀表是全局偏好(不是每个素材一份):同一台机器上所有素材、所有库共用。 +// 查看器在加载资源前读取它,用户的增删都会立即落盘。 +function createSuffixStore(userDataPath) { + const directory = path.join(userDataPath, 'SpineViewer'); + const file = path.join(directory, 'suffix-settings.json'); + return { + file, + read() { + try { + const data = JSON.parse(fs.readFileSync(file, 'utf8')); + return Array.isArray(data.suffixes) ? data.suffixes : []; + } catch (e) { + if (e.code === 'ENOENT') return []; + throw e; + } + }, + // 仅在用户通过下拉列表增删尾缀时调用。 + write(suffixes) { + if (!Array.isArray(suffixes)) throw new TypeError('尾缀设置必须为数组'); + fs.mkdirSync(directory, { recursive: true }); + const temp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp'; + try { + fs.writeFileSync(temp, JSON.stringify({ suffixes }, null, 2) + '\n', { flag: 'wx' }); + fs.renameSync(temp, file); + } finally { + try { fs.unlinkSync(temp); } catch (e) { if (e.code !== 'ENOENT') throw e; } + } + } + }; +} + +module.exports = { createStore, createSuffixStore, resolveDataPath }; diff --git a/js/spine-utils.js b/js/spine-utils.js index 6d47839..f9d1cf2 100644 --- a/js/spine-utils.js +++ b/js/spine-utils.js @@ -19,8 +19,56 @@ const fileIndexCache = new Map(); const skeletonJsonCache = new Map(); const JSON_PROBE_BYTES = 256 * 1024; -// Spine 导出常见的后缀,匹配时归一化掉(支持 - 与 _ 两种分隔符) -const SUFFIX_RE = /[-_](pro|ess|pma|base|atlas|data)$/i; +// Spine 导出常见的尾缀,匹配时归一化掉(支持 - 与 _ 两种分隔符) +// 例:hero-mesh.skel / hero-data.json / hero-pma.atlas 都归一为 hero +const DEFAULT_SUFFIXES = ['pro', 'ess', 'pma', 'base', 'atlas', 'data', 'mesh']; +// 用户可在查看器里添加自己的尾缀;为避免正则注入,只允许字母、数字、- 与 _, +// 且必须以字母或数字开头。SUFFIX_MAX_LENGTH 必须与下面的正则保持一致(有测试守着)。 +const SUFFIX_MAX_LENGTH = 50; +const SUFFIX_NAME_RE = /^[a-z0-9](?:[a-z0-9_-]{0,49})$/i; +let activeSuffixes = DEFAULT_SUFFIXES.slice(); +let suffixRe = buildSuffixRe(activeSuffixes); + +function escapeRegExp(text) { + return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function buildSuffixRe(list) { + if (!list || list.length === 0) return null; + return new RegExp('[-_](' + list.map(escapeRegExp).join('|') + ')$', 'i'); +} + +/** 校验并归一化单个尾缀名(去掉前导分隔符并转小写);非法返回 null */ +function normalizeSuffixName(raw) { + const name = String(raw == null ? '' : raw).trim().replace(/^[-_]+/, ''); + return SUFFIX_NAME_RE.test(name) ? name.toLowerCase() : null; +} + +/** + * 设置用户自定义尾缀(内置尾缀始终生效),返回当前生效的完整列表。 + * 非法或重复的名字会被安静地忽略,不信任磁盘上的内容。 + */ +function setSuffixes(list) { + const custom = []; + for (const raw of Array.isArray(list) ? list : []) { + const name = normalizeSuffixName(raw); + if (!name || DEFAULT_SUFFIXES.indexOf(name) !== -1) continue; + if (custom.indexOf(name) === -1) custom.push(name); + } + activeSuffixes = DEFAULT_SUFFIXES.concat(custom); + suffixRe = buildSuffixRe(activeSuffixes); + return getSuffixes(); +} + +/** 当前生效的尾缀(内置 + 自定义) */ +function getSuffixes() { + return activeSuffixes.slice(); +} + +/** 仅用户自定义(非内置)的尾缀,用于持久化 */ +function getCustomSuffixes() { + return activeSuffixes.filter((name) => DEFAULT_SUFFIXES.indexOf(name) === -1); +} function unsafeAtlasPath(name, reason) { const error = new Error('Atlas 贴图路径不安全:' + JSON.stringify(String(name)) + '(' + reason + ')'); @@ -132,9 +180,10 @@ function stemOf(name) { return name.slice(0, name.length - ext.length); } -/** 归一化 stem,去掉 Spine 常见后缀,用于跨文件匹配 */ +/** 归一化 stem,去掉内置与用户自定义的尾缀,用于跨文件匹配 */ function normalizeStem(name) { - return stemOf(name).toLowerCase().replace(SUFFIX_RE, ''); + const lower = stemOf(name).toLowerCase(); + return suffixRe ? lower.replace(suffixRe, '') : lower; } /** 从 JSON 文本中读取 spine 版本 */ @@ -760,6 +809,12 @@ module.exports = { isTextureFile, stemOf, normalizeStem, + DEFAULT_SUFFIXES, + SUFFIX_MAX_LENGTH, + normalizeSuffixName, + setSuffixes, + getSuffixes, + getCustomSuffixes, detectVersion, detectVersionFromJson, detectVersionFromBuffer, diff --git a/release/SpineViewer1560x1040.psd b/release/SpineViewer1560x1040.psd index 2769ba6..50926d0 100644 Binary files a/release/SpineViewer1560x1040.psd and b/release/SpineViewer1560x1040.psd differ diff --git a/release/UNIVERSAL_SpineViewer/README.md b/release/UNIVERSAL_SpineViewer/README.md index 56d6d87..0ec46d2 100644 --- a/release/UNIVERSAL_SpineViewer/README.md +++ b/release/UNIVERSAL_SpineViewer/README.md @@ -8,6 +8,7 @@ Eagle 的离线 Spine 骨骼动画预览插件,支持 Spine 3.4–4.3,适用 2. 打开骨骼或图集文件即可预览动画,并可切换动画、皮肤、速度、循环、背景和 PMA。 3. 插件不接管导入阶段的缩略图,由 Eagle 自身处理。 4. 打开预览后,原有按钮可把当前帧设为当前素材的自定义缩略图。其左侧的“为关联资源生成缩略图”会列出同 stem 的骨骼、atlas 及已验证贴图;骨骼与 atlas 默认勾选,图片和已有自定义缩略图的素材默认不勾选;用户仍可主动选择并覆盖,原图无法由插件自动恢复。 +5. 顶栏“尾缀”下拉列出内置尾缀(`pro` `ess` `pma` `base` `atlas` `data` `mesh`)与自定义尾缀:“+ 添加尾缀…”可新增,“选中自定义项”可移除。自定义尾缀保存在本地(Eagle userData),对全部素材生效,用于让 `hero-hover.skel` 这类文件与 `hero.atlas` 配成一套。 ## 安全与隐私 diff --git a/release/UNIVERSAL_SpineViewer/_locales/de_DE.json b/release/UNIVERSAL_SpineViewer/_locales/de_DE.json index 88e962e..a249ad9 100644 --- a/release/UNIVERSAL_SpineViewer/_locales/de_DE.json +++ b/release/UNIVERSAL_SpineViewer/_locales/de_DE.json @@ -91,6 +91,22 @@ "loadFailed": "Laden fehlgeschlagen", "notSpineText": "Diese Datei ist keine Spine-Ressource im Format .atlas.txt oder .skel.bytes.", "truncated256": "\n\n(nur die ersten 256 KB werden angezeigt)", + "suffix": "Suffix", + "suffixTitle": "Dateinamens-Suffixe werden beim Abgleich ignoriert, daher wird hero-mesh.skel mit hero.atlas verknüpft", + "suffixManage": "Suffixe verwalten…", + "suffixAdd": "+ Suffix hinzufügen…", + "suffixDefaults": "Integrierte Suffixe", + "suffixCustom": "Eigene Suffixe (Auswahl entfernt)", + "suffixAddTitle": "Suffix hinzufügen", + "suffixAddNote": "Suffixe werden beim Dateiabgleich ignoriert: Nach dem Hinzufügen von hover wird hero-hover.skel mit hero.atlas verknüpft. Nur Buchstaben, Ziffern, - und _, maximal 50 Zeichen.", + "suffixPlaceholder": "z. B. hover", + "suffixInvalid": "Nur Buchstaben, Ziffern, - und _, beginnend mit Buchstabe oder Ziffer", + "suffixDuplicate": "Dieses Suffix existiert bereits", + "suffixConfirm": "Hinzufügen", + "suffixRemoveTitle": "Suffix entfernen", + "suffixRemoveNote": "Nach dem Entfernen von „{name}“ werden Dateien mit diesem Suffix nicht mehr automatisch verknüpft.", + "suffixRemove": "Entfernen", + "suffixSaveFailed": "Speichern der Suffix-Liste fehlgeschlagen: {error}", "truncated1m": "\n\n(große Datei; nur das erste 1 MB wird angezeigt)", "groupWillReplaceCustom": "Ersetzt vorhandenes eigenes Vorschaubild", "groupCustomNotSelected": "Eigenes Vorschaubild vorhanden (nicht ausgewählt)" diff --git a/release/UNIVERSAL_SpineViewer/_locales/en.json b/release/UNIVERSAL_SpineViewer/_locales/en.json index 60950a5..823f1d4 100644 --- a/release/UNIVERSAL_SpineViewer/_locales/en.json +++ b/release/UNIVERSAL_SpineViewer/_locales/en.json @@ -93,6 +93,22 @@ "loadFailed": "Failed to load", "notSpineText": "This file is not a .atlas.txt or .skel.bytes Spine asset.", "truncated256": "\n\n(only the first 256 KB is shown)", + "suffix": "Suffix", + "suffixTitle": "Filename suffixes are ignored when matching, so hero-mesh.skel pairs with hero.atlas", + "suffixManage": "Manage suffixes…", + "suffixAdd": "+ Add suffix…", + "suffixDefaults": "Built-in suffixes", + "suffixCustom": "Custom suffixes (select to remove)", + "suffixAddTitle": "Add a suffix", + "suffixAddNote": "Suffixes are ignored when matching filenames: after adding hover, hero-hover.skel pairs with hero.atlas. Letters, digits, - and _ only, up to 50 characters.", + "suffixPlaceholder": "e.g. hover", + "suffixInvalid": "Use letters, digits, - and _ only, and start with a letter or digit", + "suffixDuplicate": "That suffix already exists", + "suffixConfirm": "Add", + "suffixRemoveTitle": "Remove a suffix", + "suffixRemoveNote": "After removing “{name}”, files carrying that suffix will no longer be paired automatically.", + "suffixRemove": "Remove", + "suffixSaveFailed": "Saving the suffix list failed: {error}", "truncated1m": "\n\n(large file, only the first 1 MB is shown)" } } diff --git a/release/UNIVERSAL_SpineViewer/_locales/es_ES.json b/release/UNIVERSAL_SpineViewer/_locales/es_ES.json index e53a8ba..37d57a5 100644 --- a/release/UNIVERSAL_SpineViewer/_locales/es_ES.json +++ b/release/UNIVERSAL_SpineViewer/_locales/es_ES.json @@ -91,6 +91,22 @@ "loadFailed": "Error al cargar", "notSpineText": "Este archivo no es un recurso Spine en formato .atlas.txt o .skel.bytes.", "truncated256": "\n\n(solo se muestran los primeros 256 KB)", + "suffix": "Sufijo", + "suffixTitle": "Los sufijos del nombre se ignoran al emparejar, de modo que hero-mesh.skel se une con hero.atlas", + "suffixManage": "Gestionar sufijos…", + "suffixAdd": "+ Añadir sufijo…", + "suffixDefaults": "Sufijos integrados", + "suffixCustom": "Sufijos personalizados (selecciona para quitar)", + "suffixAddTitle": "Añadir un sufijo", + "suffixAddNote": "Los sufijos se ignoran al emparejar nombres: al añadir hover, hero-hover.skel se empareja con hero.atlas. Solo letras, dígitos, - y _, hasta 50 caracteres.", + "suffixPlaceholder": "p. ej. hover", + "suffixInvalid": "Usa solo letras, dígitos, - y _, y empieza por letra o dígito", + "suffixDuplicate": "Ese sufijo ya existe", + "suffixConfirm": "Añadir", + "suffixRemoveTitle": "Quitar un sufijo", + "suffixRemoveNote": "Tras quitar «{name}», los archivos con ese sufijo dejarán de emparejarse automáticamente.", + "suffixRemove": "Quitar", + "suffixSaveFailed": "No se pudo guardar la lista de sufijos: {error}", "truncated1m": "\n\n(archivo grande; solo se muestra el primer 1 MB)", "groupWillReplaceCustom": "Reemplazará la miniatura personalizada", "groupCustomNotSelected": "Tiene miniatura personalizada (no seleccionado)" diff --git a/release/UNIVERSAL_SpineViewer/_locales/ja_JP.json b/release/UNIVERSAL_SpineViewer/_locales/ja_JP.json index 841e1b1..e9d11e1 100644 --- a/release/UNIVERSAL_SpineViewer/_locales/ja_JP.json +++ b/release/UNIVERSAL_SpineViewer/_locales/ja_JP.json @@ -91,6 +91,22 @@ "loadFailed": "読み込みに失敗しました", "notSpineText": "このファイルは .atlas.txt または .skel.bytes 形式の Spine リソースではありません。", "truncated256": "\n\n(先頭 256 KB のみ表示)", + "suffix": "接尾辞", + "suffixTitle": "ファイル名の接尾辞は照合時に無視され、hero-mesh.skel と hero.atlas が同じセットとして扱われます", + "suffixManage": "接尾辞を管理…", + "suffixAdd": "+ 接尾辞を追加…", + "suffixDefaults": "組み込みの接尾辞", + "suffixCustom": "カスタム接尾辞(選択で削除)", + "suffixAddTitle": "接尾辞を追加", + "suffixAddNote": "接尾辞はファイル名の照合時に無視されます。hover を追加すると hero-hover.skel は hero.atlas と同じセットになります。使用できるのは英数字と - _ のみ、最大 50 文字です。", + "suffixPlaceholder": "例: hover", + "suffixInvalid": "英数字と - _ のみ使用でき、先頭は英数字である必要があります", + "suffixDuplicate": "その接尾辞は既に存在します", + "suffixConfirm": "追加", + "suffixRemoveTitle": "接尾辞を削除", + "suffixRemoveNote": "「{name}」を削除すると、その接尾辞を含むファイルは自動照合されなくなります。", + "suffixRemove": "削除", + "suffixSaveFailed": "接尾辞設定の保存に失敗しました: {error}", "truncated1m": "\n\n(ファイルが大きいため、先頭 1 MB のみ表示)", "groupWillReplaceCustom": "既存のカスタムサムネイルを置き換えます", "groupCustomNotSelected": "カスタムサムネイルあり(未選択)" diff --git a/release/UNIVERSAL_SpineViewer/_locales/ko_KR.json b/release/UNIVERSAL_SpineViewer/_locales/ko_KR.json index 2ad05f1..0c6ea1d 100644 --- a/release/UNIVERSAL_SpineViewer/_locales/ko_KR.json +++ b/release/UNIVERSAL_SpineViewer/_locales/ko_KR.json @@ -91,6 +91,22 @@ "loadFailed": "불러오기 실패", "notSpineText": "이 파일은 .atlas.txt 또는 .skel.bytes 형식의 Spine 리소스가 아닙니다.", "truncated256": "\n\n(앞부분 256 KB만 표시)", + "suffix": "접미사", + "suffixTitle": "파일 이름 접미사는 매칭 시 무시되어 hero-mesh.skel 이 hero.atlas 와 같은 세트로 묶입니다", + "suffixManage": "접미사 관리…", + "suffixAdd": "+ 접미사 추가…", + "suffixDefaults": "기본 접미사", + "suffixCustom": "사용자 접미사(선택하면 제거)", + "suffixAddTitle": "접미사 추가", + "suffixAddNote": "접미사는 파일 이름 매칭 시 무시됩니다. hover 를 추가하면 hero-hover.skel 이 hero.atlas 와 같은 세트가 됩니다. 영문, 숫자, - 와 _ 만 사용할 수 있으며 최대 50자입니다.", + "suffixPlaceholder": "예: hover", + "suffixInvalid": "영문, 숫자, - 와 _ 만 사용할 수 있으며 첫 글자는 영문이나 숫자여야 합니다", + "suffixDuplicate": "이미 존재하는 접미사입니다", + "suffixConfirm": "추가", + "suffixRemoveTitle": "접미사 제거", + "suffixRemoveNote": "“{name}”을(를) 제거하면 해당 접미사가 붙은 파일은 더 이상 자동으로 매칭되지 않습니다.", + "suffixRemove": "제거", + "suffixSaveFailed": "접미사 설정을 저장하지 못했습니다: {error}", "truncated1m": "\n\n(파일이 커서 앞부분 1 MB만 표시)", "groupWillReplaceCustom": "기존 사용자 지정 썸네일 교체", "groupCustomNotSelected": "사용자 지정 썸네일 있음(선택 안 함)" diff --git a/release/UNIVERSAL_SpineViewer/_locales/ru_RU.json b/release/UNIVERSAL_SpineViewer/_locales/ru_RU.json index 074ab2a..f0ceafc 100644 --- a/release/UNIVERSAL_SpineViewer/_locales/ru_RU.json +++ b/release/UNIVERSAL_SpineViewer/_locales/ru_RU.json @@ -91,6 +91,22 @@ "loadFailed": "Не удалось загрузить", "notSpineText": "Этот файл не является ресурсом Spine в формате .atlas.txt или .skel.bytes.", "truncated256": "\n\n(показаны только первые 256 КБ)", + "suffix": "Суффикс", + "suffixTitle": "Суффиксы имён файлов игнорируются при сопоставлении, поэтому hero-mesh.skel объединяется с hero.atlas", + "suffixManage": "Управление суффиксами…", + "suffixAdd": "+ Добавить суффикс…", + "suffixDefaults": "Встроенные суффиксы", + "suffixCustom": "Пользовательские суффиксы (выбор — удалить)", + "suffixAddTitle": "Добавить суффикс", + "suffixAddNote": "Суффиксы игнорируются при сопоставлении имён: добавив hover, файл hero-hover.skel объединится с hero.atlas. Допустимы буквы, цифры, - и _, не более 50 символов.", + "suffixPlaceholder": "например hover", + "suffixInvalid": "Допустимы только буквы, цифры, - и _; первый символ — буква или цифра", + "suffixDuplicate": "Такой суффикс уже есть", + "suffixConfirm": "Добавить", + "suffixRemoveTitle": "Удалить суффикс", + "suffixRemoveNote": "После удаления «{name}» файлы с этим суффиксом больше не будут сопоставляться автоматически.", + "suffixRemove": "Удалить", + "suffixSaveFailed": "Не удалось сохранить список суффиксов: {error}", "truncated1m": "\n\n(файл большой, показан только первый 1 МБ)", "groupWillReplaceCustom": "Заменит существующую миниатюру", "groupCustomNotSelected": "Есть своя миниатюра (не выбрано)" diff --git a/release/UNIVERSAL_SpineViewer/_locales/zh_CN.json b/release/UNIVERSAL_SpineViewer/_locales/zh_CN.json index d9ff0f2..469b367 100644 --- a/release/UNIVERSAL_SpineViewer/_locales/zh_CN.json +++ b/release/UNIVERSAL_SpineViewer/_locales/zh_CN.json @@ -93,6 +93,22 @@ "loadFailed": "加载失败", "notSpineText": "此文件不是 .atlas.txt 或 .skel.bytes 格式的 Spine 资源。", "truncated256": "\n\n(仅显示前 256 KB)", + "suffix": "尾缀", + "suffixTitle": "文件名尾缀在匹配时会被忽略,使 hero-mesh.skel 与 hero.atlas 归为同一套资源", + "suffixManage": "管理尾缀…", + "suffixAdd": "+ 添加尾缀…", + "suffixDefaults": "内置尾缀", + "suffixCustom": "自定义尾缀(选中即移除)", + "suffixAddTitle": "添加尾缀", + "suffixAddNote": "尾缀会在文件名匹配时被忽略:添加 hover 后,hero-hover.skel 会与 hero.atlas 归为同一套资源。只能使用字母、数字、- 和 _,最长 50 个字符。", + "suffixPlaceholder": "例如 hover", + "suffixInvalid": "只能使用字母、数字、- 和 _,且需以字母或数字开头", + "suffixDuplicate": "该尾缀已存在", + "suffixConfirm": "添加", + "suffixRemoveTitle": "移除尾缀", + "suffixRemoveNote": "移除“{name}”后,带该尾缀的文件将不再自动配对。", + "suffixRemove": "移除", + "suffixSaveFailed": "保存尾缀设置失败:{error}", "truncated1m": "\n\n(文件较大,仅显示前 1 MB)" } } diff --git a/release/UNIVERSAL_SpineViewer/_locales/zh_TW.json b/release/UNIVERSAL_SpineViewer/_locales/zh_TW.json index 6683cdb..7155422 100644 --- a/release/UNIVERSAL_SpineViewer/_locales/zh_TW.json +++ b/release/UNIVERSAL_SpineViewer/_locales/zh_TW.json @@ -93,6 +93,22 @@ "loadFailed": "載入失敗", "notSpineText": "此檔案不是 .atlas.txt 或 .skel.bytes 格式的 Spine 資源。", "truncated256": "\n\n(僅顯示前 256 KB)", + "suffix": "尾綴", + "suffixTitle": "檔名尾綴在比對時會被忽略,讓 hero-mesh.skel 與 hero.atlas 歸為同一套資源", + "suffixManage": "管理尾綴…", + "suffixAdd": "+ 新增尾綴…", + "suffixDefaults": "內建尾綴", + "suffixCustom": "自訂尾綴(選取即移除)", + "suffixAddTitle": "新增尾綴", + "suffixAddNote": "尾綴會在檔名比對時被忽略:新增 hover 後,hero-hover.skel 會與 hero.atlas 歸為同一套資源。僅能使用字母、數字、- 和 _,最長 50 個字元。", + "suffixPlaceholder": "例如 hover", + "suffixInvalid": "僅能使用字母、數字、- 和 _,且需以字母或數字開頭", + "suffixDuplicate": "該尾綴已存在", + "suffixConfirm": "新增", + "suffixRemoveTitle": "移除尾綴", + "suffixRemoveNote": "移除「{name}」後,帶該尾綴的檔案將不再自動配對。", + "suffixRemove": "移除", + "suffixSaveFailed": "儲存尾綴設定失敗:{error}", "truncated1m": "\n\n(檔案較大,僅顯示前 1 MB)" } } diff --git a/release/UNIVERSAL_SpineViewer/js/spine-preview-settings.js b/release/UNIVERSAL_SpineViewer/js/spine-preview-settings.js index 93b6e87..dc7a3bb 100644 --- a/release/UNIVERSAL_SpineViewer/js/spine-preview-settings.js +++ b/release/UNIVERSAL_SpineViewer/js/spine-preview-settings.js @@ -55,4 +55,35 @@ async function resolveDataPath(app) { return path.join(process.env.XDG_CONFIG_HOME || path.join(home, '.config'), 'Eagle'); } -module.exports = { createStore, resolveDataPath }; +// 尾缀表是全局偏好(不是每个素材一份):同一台机器上所有素材、所有库共用。 +// 查看器在加载资源前读取它,用户的增删都会立即落盘。 +function createSuffixStore(userDataPath) { + const directory = path.join(userDataPath, 'SpineViewer'); + const file = path.join(directory, 'suffix-settings.json'); + return { + file, + read() { + try { + const data = JSON.parse(fs.readFileSync(file, 'utf8')); + return Array.isArray(data.suffixes) ? data.suffixes : []; + } catch (e) { + if (e.code === 'ENOENT') return []; + throw e; + } + }, + // 仅在用户通过下拉列表增删尾缀时调用。 + write(suffixes) { + if (!Array.isArray(suffixes)) throw new TypeError('尾缀设置必须为数组'); + fs.mkdirSync(directory, { recursive: true }); + const temp = file + '.' + crypto.randomBytes(8).toString('hex') + '.tmp'; + try { + fs.writeFileSync(temp, JSON.stringify({ suffixes }, null, 2) + '\n', { flag: 'wx' }); + fs.renameSync(temp, file); + } finally { + try { fs.unlinkSync(temp); } catch (e) { if (e.code !== 'ENOENT') throw e; } + } + } + }; +} + +module.exports = { createStore, createSuffixStore, resolveDataPath }; diff --git a/release/UNIVERSAL_SpineViewer/js/spine-utils.js b/release/UNIVERSAL_SpineViewer/js/spine-utils.js index 6d47839..f9d1cf2 100644 --- a/release/UNIVERSAL_SpineViewer/js/spine-utils.js +++ b/release/UNIVERSAL_SpineViewer/js/spine-utils.js @@ -19,8 +19,56 @@ const fileIndexCache = new Map(); const skeletonJsonCache = new Map(); const JSON_PROBE_BYTES = 256 * 1024; -// Spine 导出常见的后缀,匹配时归一化掉(支持 - 与 _ 两种分隔符) -const SUFFIX_RE = /[-_](pro|ess|pma|base|atlas|data)$/i; +// Spine 导出常见的尾缀,匹配时归一化掉(支持 - 与 _ 两种分隔符) +// 例:hero-mesh.skel / hero-data.json / hero-pma.atlas 都归一为 hero +const DEFAULT_SUFFIXES = ['pro', 'ess', 'pma', 'base', 'atlas', 'data', 'mesh']; +// 用户可在查看器里添加自己的尾缀;为避免正则注入,只允许字母、数字、- 与 _, +// 且必须以字母或数字开头。SUFFIX_MAX_LENGTH 必须与下面的正则保持一致(有测试守着)。 +const SUFFIX_MAX_LENGTH = 50; +const SUFFIX_NAME_RE = /^[a-z0-9](?:[a-z0-9_-]{0,49})$/i; +let activeSuffixes = DEFAULT_SUFFIXES.slice(); +let suffixRe = buildSuffixRe(activeSuffixes); + +function escapeRegExp(text) { + return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function buildSuffixRe(list) { + if (!list || list.length === 0) return null; + return new RegExp('[-_](' + list.map(escapeRegExp).join('|') + ')$', 'i'); +} + +/** 校验并归一化单个尾缀名(去掉前导分隔符并转小写);非法返回 null */ +function normalizeSuffixName(raw) { + const name = String(raw == null ? '' : raw).trim().replace(/^[-_]+/, ''); + return SUFFIX_NAME_RE.test(name) ? name.toLowerCase() : null; +} + +/** + * 设置用户自定义尾缀(内置尾缀始终生效),返回当前生效的完整列表。 + * 非法或重复的名字会被安静地忽略,不信任磁盘上的内容。 + */ +function setSuffixes(list) { + const custom = []; + for (const raw of Array.isArray(list) ? list : []) { + const name = normalizeSuffixName(raw); + if (!name || DEFAULT_SUFFIXES.indexOf(name) !== -1) continue; + if (custom.indexOf(name) === -1) custom.push(name); + } + activeSuffixes = DEFAULT_SUFFIXES.concat(custom); + suffixRe = buildSuffixRe(activeSuffixes); + return getSuffixes(); +} + +/** 当前生效的尾缀(内置 + 自定义) */ +function getSuffixes() { + return activeSuffixes.slice(); +} + +/** 仅用户自定义(非内置)的尾缀,用于持久化 */ +function getCustomSuffixes() { + return activeSuffixes.filter((name) => DEFAULT_SUFFIXES.indexOf(name) === -1); +} function unsafeAtlasPath(name, reason) { const error = new Error('Atlas 贴图路径不安全:' + JSON.stringify(String(name)) + '(' + reason + ')'); @@ -132,9 +180,10 @@ function stemOf(name) { return name.slice(0, name.length - ext.length); } -/** 归一化 stem,去掉 Spine 常见后缀,用于跨文件匹配 */ +/** 归一化 stem,去掉内置与用户自定义的尾缀,用于跨文件匹配 */ function normalizeStem(name) { - return stemOf(name).toLowerCase().replace(SUFFIX_RE, ''); + const lower = stemOf(name).toLowerCase(); + return suffixRe ? lower.replace(suffixRe, '') : lower; } /** 从 JSON 文本中读取 spine 版本 */ @@ -760,6 +809,12 @@ module.exports = { isTextureFile, stemOf, normalizeStem, + DEFAULT_SUFFIXES, + SUFFIX_MAX_LENGTH, + normalizeSuffixName, + setSuffixes, + getSuffixes, + getCustomSuffixes, detectVersion, detectVersionFromJson, detectVersionFromBuffer, diff --git a/release/UNIVERSAL_SpineViewer/viewer/spine.css b/release/UNIVERSAL_SpineViewer/viewer/spine.css index 08b58bc..175fb73 100644 --- a/release/UNIVERSAL_SpineViewer/viewer/spine.css +++ b/release/UNIVERSAL_SpineViewer/viewer/spine.css @@ -106,6 +106,14 @@ .thumb-dialog-row.skipped .thumb-dialog-name, .thumb-dialog-row.skipped .thumb-dialog-state { color: var(--text-dim); } .thumb-dialog-actions { display: flex; justify-content: flex-end; gap: 8px; } + .thumb-dialog-input { + background: var(--panel-2); color: var(--text); + border: 1px solid var(--border); border-radius: 4px; padding: 7px 9px; + } + .thumb-dialog-input.hidden { display: none; } + .thumb-dialog-error { color: var(--error); min-height: 18px; } + /* 收窄下拉,避免在窄窗口下把右侧按钮挤到第二行(展开后的选项不受影响)*/ + #toolbar #suffixSelect { max-width: 140px; } #stage { position: absolute; inset: 0; display: flex; overflow: hidden; } #animationPanel { position: absolute; diff --git a/release/UNIVERSAL_SpineViewer/viewer/spine.html b/release/UNIVERSAL_SpineViewer/viewer/spine.html index 3847479..383b758 100644 --- a/release/UNIVERSAL_SpineViewer/viewer/spine.html +++ b/release/UNIVERSAL_SpineViewer/viewer/spine.html @@ -25,6 +25,10 @@ 背景
+ @@ -71,6 +75,18 @@ +