Files
2026-11-06 14:11:49 +08:00

204 lines
12 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>