204 lines
12 KiB
HTML
204 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<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);
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif; font-size: 13px; }
|
||
body { display: flex; flex-direction: column; gap: 9px; padding: 14px; box-sizing: border-box; }
|
||
.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;
|
||
flex: 1 1 240px;
|
||
background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 9px 11px; }
|
||
.toggle-row input { width: 16px; height: 16px; margin: 0; accent-color: var(--accent); }
|
||
/* 窗口窄的时候整组换行,而不是把标签压成一列一个字符。 */
|
||
.toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: flex-start; }
|
||
.toolbar-group { display: flex; flex-wrap: wrap; gap: 8px; flex: 0 0 auto; }
|
||
.toolbar-group.primary { flex: 1 1 auto; flex-wrap: nowrap; min-width: 0; }
|
||
.toolbar-actions { display: flex; flex: 0 0 auto; gap: 8px; }
|
||
.toolbar-group.primary .toggle-row { min-width: 220px; }
|
||
#clear, #restoreNames { background: var(--panel); color: var(--text); border: 1px solid var(--border);
|
||
border-radius: 6px; padding: 9px 12px; font: inherit; cursor: pointer; white-space: nowrap; }
|
||
#clear:hover, #restoreNames:hover { border-color: var(--accent); color: var(--accent); }
|
||
.mode-row { display: flex; align-items: center; gap: 8px; color: var(--dim); white-space: nowrap;
|
||
flex: 0 1 auto;
|
||
background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 9px 11px; }
|
||
.mode-row select { background: var(--panel); color: var(--text); border: 1px solid var(--border);
|
||
border-radius: 6px; padding: 3px 6px; font: inherit; max-width: 200px; }
|
||
.row { display: flex; gap: 8px; align-items: baseline; color: var(--dim); }
|
||
.row b { color: var(--text); font-weight: 500; word-break: break-all; }
|
||
#stats { color: var(--accent); }
|
||
#log { flex: 1; overflow: auto; margin: 0; padding: 10px; list-style: none; min-height: 140px;
|
||
background: var(--panel); border: 1px solid var(--border); border-radius: 6px;
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.55; }
|
||
#log li { white-space: pre-wrap; word-break: break-all; }
|
||
#log li.dim { color: var(--dim); }
|
||
#log li.ok { color: var(--accent); }
|
||
#log li.fail { color: #e06c75; }
|
||
#log li.warn { color: var(--warn); }
|
||
#log li.error { color: #ff5f56; }
|
||
/* 过滤直接用 CSS:给 #log 一个 data-filter,非该类别的行就不显示,不用遍历 DOM。 */
|
||
#log[data-filter="ok"] li:not(.ok),
|
||
#log[data-filter="fail"] li:not(.fail),
|
||
#log[data-filter="warn"] li:not(.warn),
|
||
#log[data-filter="error"] li:not(.error) { display: none; }
|
||
.filters { display: flex; gap: 6px; }
|
||
.filters button { background: var(--panel); color: var(--dim); border: 1px solid var(--border);
|
||
border-radius: 999px; padding: 3px 12px; font: inherit; font-size: 12px; cursor: pointer; }
|
||
.filters button.active { color: var(--accent); border-color: var(--accent); }
|
||
.compat-dialog { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center;
|
||
padding: 18px; background: rgba(0, 0, 0, .62); }
|
||
.compat-dialog.hidden { display: none; }
|
||
.compat-card { width: min(640px, 100%); max-height: calc(100vh - 36px); display: flex;
|
||
flex-direction: column; gap: 12px; padding: 18px; background: var(--panel);
|
||
border: 1px solid var(--border); border-radius: 10px; box-shadow: 0 18px 54px rgba(0, 0, 0, .48); }
|
||
.compat-card h2 { margin: 0; font-size: 16px; }
|
||
.compat-card p { margin: 0; color: var(--dim); line-height: 1.65; }
|
||
.compat-list { overflow: auto; border: 1px solid var(--border); border-radius: 6px; }
|
||
.compat-row { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 9px;
|
||
padding: 9px 11px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
||
.compat-row:last-child { border-bottom: 0; }
|
||
.compat-row input { margin: 3px 0 0; accent-color: var(--accent); }
|
||
.compat-row-content { min-width: 0; }
|
||
.compat-file { color: var(--text); font-weight: 500; word-break: break-all; }
|
||
.compat-path { color: var(--dim); font-size: 12px; word-break: break-all; }
|
||
.compat-change { margin-top: 4px; font-family: ui-monospace, Consolas, monospace; font-size: 12px; word-break: break-all; }
|
||
.compat-change code { color: var(--accent); background: transparent; border: 0; padding: 0; }
|
||
.compat-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
||
.compat-actions button { background: var(--panel); color: var(--text); border: 1px solid var(--border);
|
||
border-radius: 6px; padding: 7px 14px; font: inherit; cursor: pointer; }
|
||
.compat-actions button.primary { color: #fff; background: #1976d2; border-color: #1976d2; }
|
||
.compat-actions button:disabled { opacity: .45; cursor: default; }
|
||
.restore-row { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 9px;
|
||
padding: 9px 11px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
||
.restore-row:last-child { border-bottom: 0; }
|
||
.restore-row.disabled { opacity: .55; cursor: default; }
|
||
.restore-row input { margin: 3px 0 0; accent-color: var(--accent); }
|
||
.restore-current, .restore-target { margin-top: 4px; font-family: ui-monospace, Consolas, monospace;
|
||
font-size: 12px; word-break: break-all; }
|
||
.restore-current code, .restore-target code { color: var(--accent); background: transparent; border: 0; padding: 0; }
|
||
.restore-status { color: var(--dim); font-size: 12px; margin-top: 3px; }
|
||
.restore-status.applied { color: #3fb950; }
|
||
.restore-row.disabled .restore-status { color: var(--warn); }
|
||
.restore-row.disabled .restore-status.applied { color: #3fb950; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="notice">
|
||
<p data-i18n="thumb.noticeIntro">功能:导入 Spine 资产(.skel / .json + .atlas + 贴图)后,本插件会为还没设置缩略图的素材生成一张骨骼预览图。</p>
|
||
<p data-i18n="thumb.noticeSafety">1. 自动生成默认关闭,需要你在下面手动启用。它只处理本次新导入的素材,不会覆盖已经设置过的自定义缩略图。</p>
|
||
<p data-i18n="thumb.noticeCompound">2. 复合后缀素材只有在确认原值和新值后,才会调整 Eagle 的名称/类型字段;原值会保存并可恢复。</p>
|
||
</div>
|
||
<div class="toolbar">
|
||
<div class="toolbar-group primary">
|
||
<label class="toggle-row"><input type="checkbox" id="toggle"><span data-i18n="thumb.autoToggle">导入时自动生成缩略图</span></label>
|
||
<div class="toolbar-actions">
|
||
<button id="clear" type="button" data-i18n="thumb.clear">清空日志</button>
|
||
<button id="restoreNames" type="button" data-i18n="thumb.restoreNames">恢复名称/类型</button>
|
||
</div>
|
||
</div>
|
||
<div class="toolbar-group">
|
||
<label class="mode-row"><span data-i18n="thumb.modeLabel">生成方式</span>
|
||
<select id="mode">
|
||
<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 data-i18n="thumb.strategyLabel">生成策略</span>
|
||
<select id="strategy">
|
||
<option value="size" data-i18n="thumb.strategySize">最优体积(慢)</option>
|
||
<option value="speed" data-i18n="thumb.strategySpeed">最优速度(快)</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
</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" 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>
|
||
<div id="compatDialog" class="compat-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="compatTitle">
|
||
<div class="compat-card">
|
||
<h2 id="compatTitle" data-i18n="thumb.compoundConfirmTitle">确认修改复合后缀</h2>
|
||
<p data-i18n="thumb.compoundConfirmIntro">由于复合后缀文件的特殊性,为了让这些素材显示生成的缩略图,Eagle 需要调整名称和类型字段。原值会保存,可随时在插件面板恢复。</p>
|
||
<div id="compatList" class="compat-list"></div>
|
||
<div class="compat-actions">
|
||
<button id="compatSkip" type="button" data-i18n="thumb.compoundSkip">跳过并继续</button>
|
||
<button id="compatConfirm" class="primary" type="button" data-i18n="thumb.compoundConfirm">确认并应用</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="restoreDialog" class="compat-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="restoreTitle">
|
||
<div class="compat-card">
|
||
<h2 id="restoreTitle" data-i18n="thumb.restoreTitle">选择要恢复的素材</h2>
|
||
<p data-i18n="thumb.restoreIntro">以下是插件保存了原值的素材。可恢复项默认全部勾选;后来被用户修改或无法读取的素材不可选择。</p>
|
||
<div id="restoreList" class="compat-list"></div>
|
||
<div class="compat-actions">
|
||
<button id="restoreCancel" type="button" data-i18n="thumb.restoreCancel">取消</button>
|
||
<button id="restoreApply" class="primary" type="button" disabled>恢复选中</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
// service 页面里的 require 不以插件目录为基准,本地资源要用绝对路径,这里先把根目录算出来。
|
||
(function () {
|
||
try {
|
||
// Windows 上 pathname 形如 /C:/...,要去掉盘符前那个多余的斜杠;
|
||
// macOS/Linux 是 /Users/...,前导斜杠必须留着,否则会变成相对路径。
|
||
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>
|
||
// 诊断用:脚本一旦抛错,直接把原因写在状态栏,避免停在「启动中…」看不出问题。
|
||
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>
|
||
<script src="js/spine-background.js"></script>
|
||
<script src="js/spine-visible-bounds.js"></script>
|
||
<script src="js/animated-profiles.js"></script>
|
||
<script src="js/animated-webp-encoder.js"></script>
|
||
<script src="js/spine-utils.js"></script>
|
||
<script src="js/spine-legacy-binary.js"></script>
|
||
<script src="js/spine-badge.js"></script>
|
||
<script src="js/settings.js"></script>
|
||
<script src="js/compound-compat.js"></script>
|
||
<script src="js/instance-coordinator.js"></script>
|
||
<script src="js/progress-window.js"></script>
|
||
<script src="js/renderer.js"></script>
|
||
<script src="js/library-watcher.js"></script>
|
||
<script src="js/spine-assets.js"></script>
|
||
<script src="js/plugin.js"></script>
|
||
</body>
|
||
</html>
|