尾缀识别
This commit is contained in:
1 parent
d9f7bd2f89
commit
94e9304d9e
34 files changed
+971
-13
No files matched your search
@@ -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;
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
<input type="checkbox" id="backgroundCheck"> <span data-i18n="viewer.background">背景</span>
|
||||
</label>
|
||||
<div class="spacer"></div>
|
||||
<label data-i18n-title="viewer.suffixTitle" title="文件名尾缀在匹配时会被忽略,使 hero-mesh.skel 与 hero.atlas 归为同一套资源">
|
||||
<span data-i18n="viewer.suffix">尾缀</span>
|
||||
<select id="suffixSelect"></select>
|
||||
</label>
|
||||
<button id="groupThumbBtn" data-i18n="viewer.groupThumbnail" data-i18n-title="viewer.groupThumbnailTitle" title="为关联资源生成缩略图">为关联资源生成缩略图</button>
|
||||
<button id="thumbBtn" data-i18n="viewer.thumbnail" data-i18n-title="viewer.thumbnailTitle" title="将当前帧设为当前素材的缩略图">设为当前素材缩略图</button>
|
||||
<label id="verLabel" style="color: var(--text-dim);" data-i18n-title="viewer.versionTitle" title="Spine 版本"></label>
|
||||
@@ -71,6 +75,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="suffixDialog" class="thumb-dialog hidden" role="dialog" aria-modal="true" aria-labelledby="suffixDialogTitle">
|
||||
<div class="thumb-dialog-card">
|
||||
<div id="suffixDialogTitle" class="thumb-dialog-title"></div>
|
||||
<div id="suffixDialogNote" class="thumb-dialog-note"></div>
|
||||
<input type="text" id="suffixDialogInput" class="thumb-dialog-input hidden" maxlength="50" autocomplete="off" spellcheck="false">
|
||||
<div id="suffixDialogError" class="thumb-dialog-error"></div>
|
||||
<div class="thumb-dialog-actions">
|
||||
<button id="suffixDialogCancel" data-i18n="viewer.cancel">取消</button>
|
||||
<button id="suffixDialogConfirm" class="primary"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -83,6 +99,7 @@
|
||||
const spineBadge = require('../js/spine-badge.js');
|
||||
const clippingRenderer = require('../js/spine-canvas-clipping.js');
|
||||
const groupThumbnails = require('../js/spine-group-thumbnails.js');
|
||||
const previewSettingsModule = require('../js/spine-preview-settings.js');
|
||||
let spineBadgeImagePromise = null;
|
||||
|
||||
function loadSpineBadgeImage() {
|
||||
@@ -1343,6 +1360,183 @@
|
||||
setStatus(t('viewer.pmaSaveFailed', { error: err.message }), 5000);
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- 文件名尾缀设置(全局偏好,实时落盘) ----------
|
||||
const SUFFIX_NONE_VALUE = '__none__';
|
||||
const SUFFIX_ADD_VALUE = '__add__';
|
||||
const SUFFIX_CUSTOM_PREFIX = 'custom:';
|
||||
const suffixSelectEl = document.getElementById('suffixSelect');
|
||||
let suffixSettings = null;
|
||||
let customSuffixes = [];
|
||||
|
||||
// 下拉列表只是入口与状态展示(内置尾缀以禁用条目列出),
|
||||
// 选择「+ 添加尾缀…」=新增,选择自定义项 =移除,两者都会先弹确认框。
|
||||
// 静止项必须是独立的值:选中“已经是当前值的项”不会触发 change,
|
||||
// 若把静止值设成新增项本身,用户点它就会毫无反应。
|
||||
function renderSuffixSelect() {
|
||||
suffixSelectEl.textContent = '';
|
||||
const resting = document.createElement('option');
|
||||
resting.value = SUFFIX_NONE_VALUE;
|
||||
resting.textContent = t('viewer.suffixManage');
|
||||
suffixSelectEl.appendChild(resting);
|
||||
const add = document.createElement('option');
|
||||
add.value = SUFFIX_ADD_VALUE;
|
||||
add.textContent = t('viewer.suffixAdd');
|
||||
suffixSelectEl.appendChild(add);
|
||||
const appendGroup = (labelKey, names, removable) => {
|
||||
if (names.length === 0) return;
|
||||
const group = document.createElement('option');
|
||||
group.disabled = true;
|
||||
group.textContent = '── ' + t(labelKey) + ' ──';
|
||||
suffixSelectEl.appendChild(group);
|
||||
for (const name of names) {
|
||||
const option = document.createElement('option');
|
||||
option.textContent = removable ? '✕ ' + name : name;
|
||||
if (removable) option.value = SUFFIX_CUSTOM_PREFIX + name;
|
||||
else option.disabled = true;
|
||||
suffixSelectEl.appendChild(option);
|
||||
}
|
||||
};
|
||||
appendGroup('viewer.suffixDefaults', utils.DEFAULT_SUFFIXES, false);
|
||||
appendGroup('viewer.suffixCustom', customSuffixes, true);
|
||||
suffixSelectEl.value = SUFFIX_NONE_VALUE;
|
||||
}
|
||||
|
||||
// 与缩略图对话框共用遮罩/卡片样式;withInput 为 false 时是纯确认框。
|
||||
function openSuffixDialog(options) {
|
||||
const dialog = document.getElementById('suffixDialog');
|
||||
const inputEl = document.getElementById('suffixDialogInput');
|
||||
const errorEl = document.getElementById('suffixDialogError');
|
||||
const confirmBtn = document.getElementById('suffixDialogConfirm');
|
||||
const cancelBtn = document.getElementById('suffixDialogCancel');
|
||||
document.getElementById('suffixDialogTitle').textContent = t(options.titleKey);
|
||||
document.getElementById('suffixDialogNote').textContent = t(options.noteKey, options.noteVars);
|
||||
confirmBtn.textContent = t(options.confirmKey);
|
||||
inputEl.classList.toggle('hidden', !options.withInput);
|
||||
inputEl.maxLength = utils.SUFFIX_MAX_LENGTH;
|
||||
inputEl.value = options.withInput ? (options.initialValue || '') : '';
|
||||
inputEl.placeholder = options.withInput ? t('viewer.suffixPlaceholder') : '';
|
||||
errorEl.textContent = '';
|
||||
dialog.classList.remove('hidden');
|
||||
if (options.withInput) { inputEl.focus(); inputEl.select(); }
|
||||
return new Promise((resolve) => {
|
||||
let done = false;
|
||||
const finish = (accepted) => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
dialog.classList.add('hidden');
|
||||
confirmBtn.removeEventListener('click', accept);
|
||||
cancelBtn.removeEventListener('click', reject);
|
||||
inputEl.removeEventListener('input', clearError);
|
||||
inputEl.removeEventListener('keydown', onKeydown);
|
||||
document.removeEventListener('keydown', onKeydown);
|
||||
resolve(accepted ? (options.withInput ? inputEl.value : true) : null);
|
||||
};
|
||||
const accept = () => {
|
||||
if (options.validate) {
|
||||
const message = options.validate(inputEl.value);
|
||||
if (message) { errorEl.textContent = message; return; }
|
||||
}
|
||||
finish(true);
|
||||
};
|
||||
const reject = () => finish(false);
|
||||
const clearError = () => { errorEl.textContent = ''; };
|
||||
const onKeydown = (event) => {
|
||||
if (event.key === 'Escape') { event.preventDefault(); reject(); }
|
||||
else if (event.key === 'Enter' && options.withInput) { event.preventDefault(); accept(); }
|
||||
};
|
||||
confirmBtn.addEventListener('click', accept);
|
||||
cancelBtn.addEventListener('click', reject);
|
||||
inputEl.addEventListener('input', clearError);
|
||||
inputEl.addEventListener('keydown', onKeydown);
|
||||
document.addEventListener('keydown', onKeydown);
|
||||
});
|
||||
}
|
||||
|
||||
function applySuffixes(list) {
|
||||
if (!suffixSettings) throw new Error(t('viewer.settingsNotReady'));
|
||||
suffixSettings.write(list);
|
||||
customSuffixes = utils.setSuffixes(list).filter((name) => utils.DEFAULT_SUFFIXES.indexOf(name) === -1);
|
||||
}
|
||||
|
||||
async function addCustomSuffix() {
|
||||
const input = await openSuffixDialog({
|
||||
titleKey: 'viewer.suffixAddTitle',
|
||||
noteKey: 'viewer.suffixAddNote',
|
||||
confirmKey: 'viewer.suffixConfirm',
|
||||
withInput: true,
|
||||
validate: (value) => {
|
||||
const name = utils.normalizeSuffixName(value);
|
||||
if (!name) return t('viewer.suffixInvalid');
|
||||
if (utils.DEFAULT_SUFFIXES.indexOf(name) !== -1 || customSuffixes.indexOf(name) !== -1) return t('viewer.suffixDuplicate');
|
||||
return null;
|
||||
},
|
||||
});
|
||||
if (input === null) return;
|
||||
try {
|
||||
applySuffixes(customSuffixes.concat([utils.normalizeSuffixName(input)]));
|
||||
// 尾缀会改变资源配对结果,直接重载查看器最稳。
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
setStatus(t('viewer.suffixSaveFailed', { error: (error && error.message) || String(error) }), 6000);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeCustomSuffix(name) {
|
||||
const accepted = await openSuffixDialog({
|
||||
titleKey: 'viewer.suffixRemoveTitle',
|
||||
noteKey: 'viewer.suffixRemoveNote',
|
||||
noteVars: { name },
|
||||
confirmKey: 'viewer.suffixRemove',
|
||||
});
|
||||
if (accepted === null) return;
|
||||
try {
|
||||
applySuffixes(customSuffixes.filter((item) => item !== name));
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
setStatus(t('viewer.suffixSaveFailed', { error: (error && error.message) || String(error) }), 6000);
|
||||
}
|
||||
}
|
||||
|
||||
// 事件回调里没有 await,任何异常都必须在这里兜住,否则会静默失败。
|
||||
function runSuffixAction(promise) {
|
||||
Promise.resolve(promise).catch((error) => {
|
||||
setStatus(t('viewer.suffixSaveFailed', { error: (error && error.message) || String(error) }), 6000);
|
||||
});
|
||||
}
|
||||
|
||||
suffixSelectEl.addEventListener('change', (event) => {
|
||||
const value = event.target.value;
|
||||
// 先复位到静止项,保证下一次选择一定会产生 change。
|
||||
renderSuffixSelect();
|
||||
if (value === SUFFIX_ADD_VALUE) runSuffixAction(addCustomSuffix());
|
||||
else if (value.indexOf(SUFFIX_CUSTOM_PREFIX) === 0) runSuffixAction(removeCustomSuffix(value.slice(SUFFIX_CUSTOM_PREFIX.length)));
|
||||
});
|
||||
|
||||
async function initSuffixSettings() {
|
||||
try {
|
||||
const userDataPath = await previewSettingsModule.resolveDataPath(typeof eagle !== 'undefined' ? eagle.app : null);
|
||||
suffixSettings = previewSettingsModule.createSuffixStore(userDataPath);
|
||||
} catch (error) {
|
||||
suffixSettings = null;
|
||||
log('尾缀设置存储不可用: ' + ((error && error.message) || String(error)));
|
||||
}
|
||||
try {
|
||||
if (suffixSettings) {
|
||||
// 磁盘内容不可信:utils 会丢弃非法、重复和内置重名的尾缀。
|
||||
customSuffixes = utils.setSuffixes(suffixSettings.read()).filter((name) => utils.DEFAULT_SUFFIXES.indexOf(name) === -1);
|
||||
} else {
|
||||
utils.setSuffixes([]);
|
||||
customSuffixes = [];
|
||||
}
|
||||
} catch (error) {
|
||||
// 配置损坏时回退到内置尾缀,但保留存储对象,下次写入会覆盖它。
|
||||
customSuffixes = utils.getCustomSuffixes();
|
||||
log('尾缀配置损坏,已回退到内置尾缀: ' + ((error && error.message) || String(error)));
|
||||
}
|
||||
renderSuffixSelect();
|
||||
}
|
||||
|
||||
document.getElementById('reloadBtn').addEventListener('click', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
@@ -1434,7 +1628,7 @@
|
||||
} else if (!fs.existsSync(filePath)) {
|
||||
showError(t('viewer.fileNotExist'), filePath);
|
||||
} else {
|
||||
loadAssets().catch((err) => {
|
||||
initSuffixSettings().then(() => loadAssets()).catch((err) => {
|
||||
showError(t('viewer.loadFailed'), (err && err.message) ? err.message : String(err));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user